From 44780837f1d991f5e0ba0be6c7cc12f5beb336af Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Aug 2019 06:33:10 +0200 Subject: [PATCH 001/563] Version bump to 2019-8 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 14f0bb819..1706d610f 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.7-dev' +__version__ = '2019.8' class DependencyException(Exception): From 040ba5662c970a5c2a54aca295a479810ed2421e Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Fri, 30 Aug 2019 01:54:16 +0300 Subject: [PATCH 002/563] Merge pull request #2199 from freqtrade/fix_datadir_init Fix datadir init to always include exchange --- freqtrade/configuration/configuration.py | 14 ++++++++------ freqtrade/tests/test_configuration.py | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index f5480c53a..b1bd3ef1c 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -4,6 +4,7 @@ This module contains the configuration class import logging import warnings from argparse import Namespace +from copy import deepcopy from pathlib import Path from typing import Any, Callable, Dict, List, Optional @@ -56,7 +57,7 @@ class Configuration(object): config: Dict[str, Any] = {} if not files: - return constants.MINIMAL_CONFIG.copy() + return deepcopy(constants.MINIMAL_CONFIG) # We expect here a list of config filenames for path in files: @@ -160,6 +161,11 @@ class Configuration(object): Extract information for sys.argv and load directory configurations --user-data, --datadir """ + # Check exchange parameter here - otherwise `datadir` might be wrong. + if "exchange" in self.args and self.args.exchange: + config['exchange']['name'] = self.args.exchange + logger.info(f"Using exchange {config['exchange']['name']}") + if 'user_data_dir' in self.args and self.args.user_data_dir: config.update({'user_data_dir': self.args.user_data_dir}) elif 'user_data_dir' not in config: @@ -297,10 +303,6 @@ class Configuration(object): self._args_to_config(config, argname='days', logstring='Detected --days: {}') - if "exchange" in self.args and self.args.exchange: - config['exchange']['name'] = self.args.exchange - logger.info(f"Using exchange {config['exchange']['name']}") - def _process_runmode(self, config: Dict[str, Any]) -> None: if not self.runmode: @@ -361,7 +363,7 @@ class Configuration(object): config['pairs'] = config.get('exchange', {}).get('pair_whitelist') else: # Fall back to /dl_path/pairs.json - pairs_file = Path(config['datadir']) / config['exchange']['name'].lower() / "pairs.json" + pairs_file = Path(config['datadir']) / "pairs.json" if pairs_file.exists(): with pairs_file.open('r') as f: config['pairs'] = json_load(f) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 10ce7e8cf..153523f2a 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -871,3 +871,4 @@ def test_pairlist_resolving_fallback(mocker): assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] assert config['exchange']['name'] == 'binance' + assert config['datadir'] == str(Path.cwd() / "user_data/data/binance") From 7c36e571d22b748ffbc21308f0a364c3b119a9c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Aug 2019 15:38:38 +0200 Subject: [PATCH 003/563] version bump to 2019.8-1 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 1706d610f..bd44b9eb7 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.8' +__version__ = '2019.8-1' class DependencyException(Exception): From 89bba6f776196a46a8a3df62fabeeb0e9548dce7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Oct 2019 06:25:30 +0100 Subject: [PATCH 004/563] Version bump --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 6ec7bce21..b215ff44f 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.9' +__version__ = '2019.10' if __version__ == 'develop': From a74b941b72ec3d409232213660081be1480771a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Oct 2019 14:30:01 +0100 Subject: [PATCH 005/563] Add test to verify this is correct --- tests/data/test_btanalysis.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 4068e00e4..527300af7 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock import pytest from arrow import Arrow -from pandas import DataFrame, to_datetime +from pandas import DataFrame, DateOffset, to_datetime from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, @@ -134,3 +134,21 @@ def test_create_cum_profit(testdatadir): assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 + + +def test_create_cum_profit1(testdatadir): + filename = testdatadir / "backtest-result_test.json" + bt_data = load_backtest_data(filename) + # Move close-time to "off" the candle, to make sure the logic still works + bt_data.loc[:, 'close_time'] = bt_data.loc[:, 'close_time'] + DateOffset(seconds=20) + timerange = TimeRange.parse_timerange("20180110-20180112") + + df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', + datadir=testdatadir, timerange=timerange) + + cum_profits = create_cum_profit(df.set_index('date'), + bt_data[bt_data["pair"] == 'POWR/BTC'], + "cum_profits", timeframe="5m") + assert "cum_profits" in cum_profits.columns + assert cum_profits.iloc[0]['cum_profits'] == 0 + assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 From dab4ab78fce64908b6f167eb5fbc2929753d2bc9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Oct 2019 14:24:12 +0100 Subject: [PATCH 006/563] Fix create_cum_profit to work with trades that don't open on candle opens --- freqtrade/data/btanalysis.py | 12 +++++++++--- freqtrade/plot/plotting.py | 10 +++++----- tests/data/test_btanalysis.py | 2 +- tests/test_plotting.py | 4 ++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 17abae3b6..0f5d395ff 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -150,15 +150,21 @@ def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "c return df_comb -def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str) -> pd.DataFrame: +def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, + timeframe: str) -> pd.DataFrame: """ Adds a column `col_name` with the cumulative profit for the given trades array. :param df: DataFrame with date index :param trades: DataFrame containing trades (requires columns close_time and profitperc) + :param col_name: Column name that will be assigned the results + :param timeframe: Timeframe used during the operations :return: Returns df with one additional column, col_name, containing the cumulative profit. """ - # Use groupby/sum().cumsum() to avoid errors when multiple trades sold at the same candle. - df[col_name] = trades.groupby('close_time')['profitperc'].sum().cumsum() + from freqtrade.exchange import timeframe_to_minutes + ticker_minutes = timeframe_to_minutes(timeframe) + # Resample to ticker_interval to make sure trades match candles + _trades_sum = trades.resample(f'{ticker_minutes}min', on='close_time')[['profitperc']].sum() + df.loc[:, col_name] = _trades_sum.cumsum() # Set first value to 0 df.loc[df.iloc[0].name, col_name] = 0 # FFill to get continuous diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 6bd5993b6..bbdb52ca1 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -264,12 +264,12 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], - trades: pd.DataFrame) -> go.Figure: + trades: pd.DataFrame, timeframe: str) -> 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') + df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe) # Plot the pairs average close prices, and total profit growth avgclose = go.Scatter( @@ -293,7 +293,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], for pair in pairs: profit_col = f'cum_profit_{pair}' - df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) + 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}") @@ -382,9 +382,9 @@ def plot_profit(config: Dict[str, Any]) -> None: ) # 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) + fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], + trades, config.get('ticker_interval', '5m')) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / "plot", auto_open=True) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 527300af7..a04a2c529 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -130,7 +130,7 @@ def test_create_cum_profit(testdatadir): cum_profits = create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'POWR/BTC'], - "cum_profits") + "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index a39b2b76e..1c7d1b392 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -234,7 +234,7 @@ def test_add_profit(testdatadir): cum_profits = create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'POWR/BTC'], - "cum_profits") + "cum_profits", timeframe="5m") fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits') figure = fig1.layout.figure @@ -256,7 +256,7 @@ def test_generate_profit_graph(testdatadir): ) trades = trades[trades['pair'].isin(pairs)] - fig = generate_profit_graph(pairs, tickers, trades) + fig = generate_profit_graph(pairs, tickers, trades, timeframe="5m") assert isinstance(fig, go.Figure) assert fig.layout.title.text == "Freqtrade Profit plot" From 7204227022827ee074211cc3b458294777f460f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Oct 2019 09:20:56 +0100 Subject: [PATCH 007/563] Replace coins in whitelist with existing ones --- config.json.example | 2 +- config_full.json.example | 2 +- tests/config_test_comments.json | 2 +- tests/data/test_btanalysis.py | 8 ++++---- tests/optimize/test_hyperopt.py | 2 +- tests/test_plotting.py | 10 +++++----- tests/testdata/{POWR_BTC-5m.json => TRX_BTC-5m.json} | 0 tests/testdata/backtest-result_test.json | 2 +- tests/testdata/pairs.json | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) rename tests/testdata/{POWR_BTC-5m.json => TRX_BTC-5m.json} (100%) diff --git a/config.json.example b/config.json.example index 419019030..9a6dafd04 100644 --- a/config.json.example +++ b/config.json.example @@ -44,7 +44,7 @@ "ZEC/BTC", "XLM/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "ADA/BTC", "XMR/BTC" ], diff --git a/config_full.json.example b/config_full.json.example index 5789e49ac..5edc19419 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -78,7 +78,7 @@ "ZEC/BTC", "XLM/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "ADA/BTC", "XMR/BTC" ], diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json index 8af39d6ba..8f41b08fa 100644 --- a/tests/config_test_comments.json +++ b/tests/config_test_comments.json @@ -78,7 +78,7 @@ "ZEC/BTC", "XLM/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "ADA/BTC", "XMR/BTC" ], diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index a04a2c529..78781cffd 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -125,11 +125,11 @@ def test_create_cum_profit(testdatadir): bt_data = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', + df = load_pair_history(pair="TRX/BTC", ticker_interval='5m', datadir=testdatadir, timerange=timerange) cum_profits = create_cum_profit(df.set_index('date'), - bt_data[bt_data["pair"] == 'POWR/BTC'], + bt_data[bt_data["pair"] == 'TRX/BTC'], "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 @@ -143,11 +143,11 @@ def test_create_cum_profit1(testdatadir): bt_data.loc[:, 'close_time'] = bt_data.loc[:, 'close_time'] + DateOffset(seconds=20) timerange = TimeRange.parse_timerange("20180110-20180112") - df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', + df = load_pair_history(pair="TRX/BTC", ticker_interval='5m', datadir=testdatadir, timerange=timerange) cum_profits = create_cum_profit(df.set_index('date'), - bt_data[bt_data["pair"] == 'POWR/BTC'], + bt_data[bt_data["pair"] == 'TRX/BTC'], "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 052c3ba77..f82ce2083 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -516,7 +516,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: default_conf.update({'hyperopt_min_trades': 1}) trades = [ - ('POWR/BTC', 0.023117, 0.000233, 100) + ('TRX/BTC', 0.023117, 0.000233, 100) ] labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration'] backtest_result = pd.DataFrame.from_records(trades, columns=labels) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 1c7d1b392..4a6efcd8e 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -53,10 +53,10 @@ def test_init_plotscript(default_conf, mocker, testdatadir): assert "trades" in ret assert "pairs" in ret - default_conf['pairs'] = ["POWR/BTC", "ADA/BTC"] + default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"] ret = init_plotscript(default_conf) assert "tickers" in ret - assert "POWR/BTC" in ret["tickers"] + assert "TRX/BTC" in ret["tickers"] assert "ADA/BTC" in ret["tickers"] @@ -228,12 +228,12 @@ def test_add_profit(testdatadir): bt_data = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m', + df = history.load_pair_history(pair="TRX/BTC", ticker_interval='5m', datadir=testdatadir, timerange=timerange) fig = generate_empty_figure() cum_profits = create_cum_profit(df.set_index('date'), - bt_data[bt_data["pair"] == 'POWR/BTC'], + bt_data[bt_data["pair"] == 'TRX/BTC'], "cum_profits", timeframe="5m") fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits') @@ -247,7 +247,7 @@ def test_generate_profit_graph(testdatadir): filename = testdatadir / "backtest-result_test.json" trades = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - pairs = ["POWR/BTC", "ADA/BTC"] + pairs = ["TRX/BTC", "ADA/BTC"] tickers = history.load_data(datadir=testdatadir, pairs=pairs, diff --git a/tests/testdata/POWR_BTC-5m.json b/tests/testdata/TRX_BTC-5m.json similarity index 100% rename from tests/testdata/POWR_BTC-5m.json rename to tests/testdata/TRX_BTC-5m.json diff --git a/tests/testdata/backtest-result_test.json b/tests/testdata/backtest-result_test.json index 8701451dc..dce22acaf 100644 --- a/tests/testdata/backtest-result_test.json +++ b/tests/testdata/backtest-result_test.json @@ -1 +1 @@ -[["POWR/BTC",0.03990025,1515568500.0,1515568800.0,27,5,9.64e-05,0.00010074887218045112,false,"roi"],["ADA/BTC",0.03990025,1515568500.0,1515569400.0,27,15,4.756e-05,4.9705563909774425e-05,false,"roi"],["XLM/BTC",0.03990025,1515569100.0,1515569700.0,29,10,3.339e-05,3.489631578947368e-05,false,"roi"],["POWR/BTC",0.03990025,1515569100.0,1515570000.0,29,15,9.696e-05,0.00010133413533834584,false,"roi"],["ETH/BTC",-0.0,1515569700.0,1515573300.0,31,60,0.0943,0.09477268170426063,false,"roi"],["XMR/BTC",0.00997506,1515570000.0,1515571800.0,32,30,0.02719607,0.02760503345864661,false,"roi"],["ZEC/BTC",0.0,1515572100.0,1515578100.0,39,100,0.04634952,0.046581848421052625,false,"roi"],["NXT/BTC",-0.0,1515595500.0,1515599400.0,117,65,3.066e-05,3.081368421052631e-05,false,"roi"],["LTC/BTC",0.0,1515602100.0,1515604500.0,139,40,0.0168999,0.016984611278195488,false,"roi"],["ETH/BTC",-0.0,1515602400.0,1515604800.0,140,40,0.09132568,0.0917834528320802,false,"roi"],["ETH/BTC",-0.0,1515610200.0,1515613500.0,166,55,0.08898003,0.08942604518796991,false,"roi"],["ETH/BTC",0.0,1515622500.0,1515625200.0,207,45,0.08560008,0.08602915308270676,false,"roi"],["ETC/BTC",0.00997506,1515624600.0,1515626400.0,214,30,0.00249083,0.0025282860902255634,false,"roi"],["NXT/BTC",-0.0,1515626100.0,1515629700.0,219,60,3.022e-05,3.037147869674185e-05,false,"roi"],["ETC/BTC",0.01995012,1515627600.0,1515629100.0,224,25,0.002437,0.0024980776942355883,false,"roi"],["ZEC/BTC",0.00997506,1515628800.0,1515630900.0,228,35,0.04771803,0.04843559436090225,false,"roi"],["XLM/BTC",-0.10448878,1515642000.0,1515644700.0,272,45,3.651e-05,3.2859000000000005e-05,false,"stop_loss"],["ETH/BTC",0.00997506,1515642900.0,1515644700.0,275,30,0.08824105,0.08956798308270676,false,"roi"],["ETC/BTC",-0.0,1515643200.0,1515646200.0,276,50,0.00243,0.002442180451127819,false,"roi"],["ZEC/BTC",0.01995012,1515645000.0,1515646500.0,282,25,0.04545064,0.046589753784461146,false,"roi"],["XLM/BTC",0.01995012,1515645000.0,1515646200.0,282,20,3.372e-05,3.456511278195488e-05,false,"roi"],["XMR/BTC",0.01995012,1515646500.0,1515647700.0,287,20,0.02644,0.02710265664160401,false,"roi"],["ETH/BTC",-0.0,1515669600.0,1515672000.0,364,40,0.08812,0.08856170426065162,false,"roi"],["XMR/BTC",-0.0,1515670500.0,1515672900.0,367,40,0.02683577,0.026970285137844607,false,"roi"],["ADA/BTC",0.01995012,1515679200.0,1515680700.0,396,25,4.919e-05,5.04228320802005e-05,false,"roi"],["ETH/BTC",-0.0,1515698700.0,1515702900.0,461,70,0.08784896,0.08828930566416039,false,"roi"],["ADA/BTC",-0.0,1515710100.0,1515713400.0,499,55,5.105e-05,5.130588972431077e-05,false,"roi"],["XLM/BTC",0.00997506,1515711300.0,1515713100.0,503,30,3.96e-05,4.019548872180451e-05,false,"roi"],["NXT/BTC",-0.0,1515711300.0,1515713700.0,503,40,2.885e-05,2.899461152882205e-05,false,"roi"],["XMR/BTC",0.00997506,1515713400.0,1515715500.0,510,35,0.02645,0.026847744360902256,false,"roi"],["ZEC/BTC",-0.0,1515714900.0,1515719700.0,515,80,0.048,0.04824060150375939,false,"roi"],["XLM/BTC",0.01995012,1515791700.0,1515793200.0,771,25,4.692e-05,4.809593984962405e-05,false,"roi"],["ETC/BTC",-0.0,1515804900.0,1515824400.0,815,325,0.00256966,0.0025825405012531327,false,"roi"],["ADA/BTC",0.0,1515840900.0,1515843300.0,935,40,6.262e-05,6.293388471177944e-05,false,"roi"],["XLM/BTC",0.0,1515848700.0,1516025400.0,961,2945,4.73e-05,4.753709273182957e-05,false,"roi"],["ADA/BTC",-0.0,1515850200.0,1515854700.0,966,75,6.063e-05,6.0933909774436085e-05,false,"roi"],["POWR/BTC",-0.0,1515850800.0,1515886200.0,968,590,0.00011082,0.00011137548872180449,false,"roi"],["ADA/BTC",-0.0,1515856500.0,1515858900.0,987,40,5.93e-05,5.9597243107769415e-05,false,"roi"],["ZEC/BTC",-0.0,1515861000.0,1515863400.0,1002,40,0.04850003,0.04874313791979949,false,"roi"],["ETH/BTC",-0.0,1515881100.0,1515911100.0,1069,500,0.09825019,0.09874267215538847,false,"roi"],["ADA/BTC",0.0,1515889200.0,1515970500.0,1096,1355,6.018e-05,6.048165413533834e-05,false,"roi"],["ETH/BTC",-0.0,1515933900.0,1515936300.0,1245,40,0.09758999,0.0980791628822055,false,"roi"],["ETC/BTC",0.00997506,1515943800.0,1515945600.0,1278,30,0.00311,0.0031567669172932328,false,"roi"],["ETC/BTC",-0.0,1515962700.0,1515968100.0,1341,90,0.00312401,0.003139669197994987,false,"roi"],["LTC/BTC",0.0,1515972900.0,1515976200.0,1375,55,0.0174679,0.017555458395989976,false,"roi"],["DASH/BTC",-0.0,1515973500.0,1515975900.0,1377,40,0.07346846,0.07383672295739348,false,"roi"],["ETH/BTC",-0.0,1515983100.0,1515985500.0,1409,40,0.097994,0.09848519799498745,false,"roi"],["ETH/BTC",-0.0,1516000800.0,1516003200.0,1468,40,0.09659,0.09707416040100249,false,"roi"],["POWR/BTC",0.00997506,1516004400.0,1516006500.0,1480,35,9.987e-05,0.00010137180451127818,false,"roi"],["ETH/BTC",0.0,1516018200.0,1516071000.0,1526,880,0.0948969,0.09537257368421052,false,"roi"],["DASH/BTC",-0.0,1516025400.0,1516038000.0,1550,210,0.071,0.07135588972431077,false,"roi"],["ZEC/BTC",-0.0,1516026600.0,1516029000.0,1554,40,0.04600501,0.046235611553884705,false,"roi"],["POWR/BTC",-0.0,1516039800.0,1516044300.0,1598,75,9.438e-05,9.485308270676691e-05,false,"roi"],["XMR/BTC",-0.0,1516041300.0,1516043700.0,1603,40,0.03040001,0.030552391002506264,false,"roi"],["ADA/BTC",-0.10448878,1516047900.0,1516091100.0,1625,720,5.837e-05,5.2533e-05,false,"stop_loss"],["ZEC/BTC",-0.0,1516048800.0,1516053600.0,1628,80,0.046036,0.04626675689223057,false,"roi"],["ETC/BTC",-0.0,1516062600.0,1516065000.0,1674,40,0.0028685,0.0028828784461152877,false,"roi"],["DASH/BTC",0.0,1516065300.0,1516070100.0,1683,80,0.06731755,0.0676549813283208,false,"roi"],["ETH/BTC",0.0,1516088700.0,1516092000.0,1761,55,0.09217614,0.09263817578947368,false,"roi"],["LTC/BTC",0.01995012,1516091700.0,1516092900.0,1771,20,0.0165,0.016913533834586467,false,"roi"],["POWR/BTC",0.03990025,1516091700.0,1516092000.0,1771,5,7.953e-05,8.311781954887218e-05,false,"roi"],["ZEC/BTC",-0.0,1516092300.0,1516096200.0,1773,65,0.045202,0.04542857644110275,false,"roi"],["ADA/BTC",0.00997506,1516094100.0,1516095900.0,1779,30,5.248e-05,5.326917293233082e-05,false,"roi"],["XMR/BTC",0.0,1516094100.0,1516096500.0,1779,40,0.02892318,0.02906815834586466,false,"roi"],["ADA/BTC",0.01995012,1516096200.0,1516097400.0,1786,20,5.158e-05,5.287273182957392e-05,false,"roi"],["ZEC/BTC",0.00997506,1516097100.0,1516099200.0,1789,35,0.04357584,0.044231115789473675,false,"roi"],["XMR/BTC",0.00997506,1516097100.0,1516098900.0,1789,30,0.02828232,0.02870761804511278,false,"roi"],["ADA/BTC",0.00997506,1516110300.0,1516112400.0,1833,35,5.362e-05,5.4426315789473676e-05,false,"roi"],["ADA/BTC",-0.0,1516123800.0,1516127100.0,1878,55,5.302e-05,5.328576441102756e-05,false,"roi"],["ETH/BTC",0.00997506,1516126500.0,1516128300.0,1887,30,0.09129999,0.09267292218045112,false,"roi"],["XLM/BTC",0.01995012,1516126500.0,1516127700.0,1887,20,3.808e-05,3.903438596491228e-05,false,"roi"],["XMR/BTC",0.00997506,1516129200.0,1516131000.0,1896,30,0.02811012,0.028532828571428567,false,"roi"],["ETC/BTC",-0.10448878,1516137900.0,1516141500.0,1925,60,0.00258379,0.002325411,false,"stop_loss"],["NXT/BTC",-0.10448878,1516137900.0,1516142700.0,1925,80,2.559e-05,2.3031e-05,false,"stop_loss"],["POWR/BTC",-0.10448878,1516138500.0,1516141500.0,1927,50,7.62e-05,6.858e-05,false,"stop_loss"],["LTC/BTC",0.03990025,1516141800.0,1516142400.0,1938,10,0.0151,0.015781203007518795,false,"roi"],["ETC/BTC",0.03990025,1516141800.0,1516142100.0,1938,5,0.00229844,0.002402129022556391,false,"roi"],["ETC/BTC",0.03990025,1516142400.0,1516142700.0,1940,5,0.00235676,0.00246308,false,"roi"],["DASH/BTC",0.01995012,1516142700.0,1516143900.0,1941,20,0.0630692,0.06464988170426066,false,"roi"],["NXT/BTC",0.03990025,1516143000.0,1516143300.0,1942,5,2.2e-05,2.2992481203007514e-05,false,"roi"],["ADA/BTC",0.00997506,1516159800.0,1516161600.0,1998,30,4.974e-05,5.048796992481203e-05,false,"roi"],["POWR/BTC",0.01995012,1516161300.0,1516162500.0,2003,20,7.108e-05,7.28614536340852e-05,false,"roi"],["ZEC/BTC",-0.0,1516181700.0,1516184100.0,2071,40,0.04327,0.04348689223057644,false,"roi"],["ADA/BTC",-0.0,1516184400.0,1516208400.0,2080,400,4.997e-05,5.022047619047618e-05,false,"roi"],["DASH/BTC",-0.0,1516185000.0,1516188300.0,2082,55,0.06836818,0.06871087764411027,false,"roi"],["XLM/BTC",-0.0,1516185000.0,1516187400.0,2082,40,3.63e-05,3.648195488721804e-05,false,"roi"],["XMR/BTC",-0.0,1516192200.0,1516226700.0,2106,575,0.0281,0.02824085213032581,false,"roi"],["ETH/BTC",-0.0,1516192500.0,1516208100.0,2107,260,0.08651001,0.08694364413533832,false,"roi"],["ADA/BTC",-0.0,1516251600.0,1516254900.0,2304,55,5.633e-05,5.6612355889724306e-05,false,"roi"],["DASH/BTC",0.00997506,1516252800.0,1516254900.0,2308,35,0.06988494,0.07093584135338346,false,"roi"],["ADA/BTC",-0.0,1516260900.0,1516263300.0,2335,40,5.545e-05,5.572794486215538e-05,false,"roi"],["LTC/BTC",-0.0,1516266000.0,1516268400.0,2352,40,0.01633527,0.016417151052631574,false,"roi"],["ETC/BTC",-0.0,1516293600.0,1516296000.0,2444,40,0.00269734,0.0027108605012531326,false,"roi"],["XLM/BTC",0.01995012,1516298700.0,1516300200.0,2461,25,4.475e-05,4.587155388471177e-05,false,"roi"],["NXT/BTC",0.00997506,1516299900.0,1516301700.0,2465,30,2.79e-05,2.8319548872180444e-05,false,"roi"],["ZEC/BTC",0.0,1516306200.0,1516308600.0,2486,40,0.04439326,0.04461578260651629,false,"roi"],["XLM/BTC",0.0,1516311000.0,1516322100.0,2502,185,4.49e-05,4.51250626566416e-05,false,"roi"],["XMR/BTC",-0.0,1516312500.0,1516338300.0,2507,430,0.02855,0.028693107769423555,false,"roi"],["ADA/BTC",0.0,1516313400.0,1516315800.0,2510,40,5.796e-05,5.8250526315789473e-05,false,"roi"],["ZEC/BTC",0.0,1516319400.0,1516321800.0,2530,40,0.04340323,0.04362079005012531,false,"roi"],["ZEC/BTC",0.0,1516380300.0,1516383300.0,2733,50,0.04454455,0.04476783095238095,false,"roi"],["ADA/BTC",-0.0,1516382100.0,1516391700.0,2739,160,5.62e-05,5.648170426065162e-05,false,"roi"],["XLM/BTC",-0.0,1516382400.0,1516392900.0,2740,175,4.339e-05,4.360749373433584e-05,false,"roi"],["POWR/BTC",0.0,1516423500.0,1516469700.0,2877,770,0.0001009,0.00010140576441102757,false,"roi"],["ETC/BTC",-0.0,1516423800.0,1516461300.0,2878,625,0.00270505,0.002718609147869674,false,"roi"],["XMR/BTC",-0.0,1516423800.0,1516431600.0,2878,130,0.03000002,0.030150396040100245,false,"roi"],["ADA/BTC",-0.0,1516438800.0,1516441200.0,2928,40,5.46e-05,5.4873684210526304e-05,false,"roi"],["XMR/BTC",-0.10448878,1516472700.0,1516852200.0,3041,6325,0.03082222,0.027739998000000002,false,"stop_loss"],["ETH/BTC",-0.0,1516487100.0,1516490100.0,3089,50,0.08969999,0.09014961401002504,false,"roi"],["LTC/BTC",0.0,1516503000.0,1516545000.0,3142,700,0.01632501,0.01640683962406015,false,"roi"],["DASH/BTC",-0.0,1516530000.0,1516532400.0,3232,40,0.070538,0.07089157393483708,false,"roi"],["ADA/BTC",-0.0,1516549800.0,1516560300.0,3298,175,5.301e-05,5.3275714285714276e-05,false,"roi"],["XLM/BTC",0.0,1516551600.0,1516554000.0,3304,40,3.955e-05,3.9748245614035085e-05,false,"roi"],["ETC/BTC",0.00997506,1516569300.0,1516571100.0,3363,30,0.00258505,0.002623922932330827,false,"roi"],["XLM/BTC",-0.0,1516569300.0,1516571700.0,3363,40,3.903e-05,3.922563909774435e-05,false,"roi"],["ADA/BTC",-0.0,1516581300.0,1516617300.0,3403,600,5.236e-05,5.262245614035087e-05,false,"roi"],["POWR/BTC",0.0,1516584600.0,1516587000.0,3414,40,9.028e-05,9.073253132832079e-05,false,"roi"],["ETC/BTC",-0.0,1516623900.0,1516631700.0,3545,130,0.002687,0.002700468671679198,false,"roi"],["XLM/BTC",-0.0,1516626900.0,1516629300.0,3555,40,4.168e-05,4.1888922305764405e-05,false,"roi"],["POWR/BTC",0.00997506,1516629600.0,1516631400.0,3564,30,8.821e-05,8.953646616541353e-05,false,"roi"],["ADA/BTC",-0.0,1516636500.0,1516639200.0,3587,45,5.172e-05,5.1979248120300745e-05,false,"roi"],["NXT/BTC",0.01995012,1516637100.0,1516638300.0,3589,20,3.026e-05,3.101839598997494e-05,false,"roi"],["DASH/BTC",0.0,1516650600.0,1516666200.0,3634,260,0.07064,0.07099408521303258,false,"roi"],["LTC/BTC",0.0,1516656300.0,1516658700.0,3653,40,0.01644483,0.01652726022556391,false,"roi"],["XLM/BTC",0.00997506,1516665900.0,1516667700.0,3685,30,4.331e-05,4.3961278195488714e-05,false,"roi"],["NXT/BTC",0.01995012,1516672200.0,1516673700.0,3706,25,3.2e-05,3.2802005012531326e-05,false,"roi"],["ETH/BTC",0.0,1516681500.0,1516684500.0,3737,50,0.09167706,0.09213659413533835,false,"roi"],["DASH/BTC",0.0,1516692900.0,1516698000.0,3775,85,0.0692498,0.06959691679197995,false,"roi"],["NXT/BTC",0.0,1516704600.0,1516712700.0,3814,135,3.182e-05,3.197949874686716e-05,false,"roi"],["ZEC/BTC",-0.0,1516705500.0,1516723500.0,3817,300,0.04088,0.04108491228070175,false,"roi"],["ADA/BTC",-0.0,1516719300.0,1516721700.0,3863,40,5.15e-05,5.175814536340851e-05,false,"roi"],["ETH/BTC",0.0,1516725300.0,1516752300.0,3883,450,0.09071698,0.09117170170426064,false,"roi"],["NXT/BTC",-0.0,1516728300.0,1516733100.0,3893,80,3.128e-05,3.1436791979949865e-05,false,"roi"],["POWR/BTC",-0.0,1516738500.0,1516744800.0,3927,105,9.555e-05,9.602894736842104e-05,false,"roi"],["ZEC/BTC",-0.0,1516746600.0,1516749000.0,3954,40,0.04080001,0.041004521328320796,false,"roi"],["ADA/BTC",-0.0,1516751400.0,1516764900.0,3970,225,5.163e-05,5.1888796992481196e-05,false,"roi"],["ZEC/BTC",0.0,1516753200.0,1516758600.0,3976,90,0.04040781,0.04061035541353383,false,"roi"],["ADA/BTC",-0.0,1516776300.0,1516778700.0,4053,40,5.132e-05,5.157724310776942e-05,false,"roi"],["ADA/BTC",0.03990025,1516803300.0,1516803900.0,4143,10,5.198e-05,5.432496240601503e-05,false,"roi"],["NXT/BTC",-0.0,1516805400.0,1516811700.0,4150,105,3.054e-05,3.069308270676692e-05,false,"roi"],["POWR/BTC",0.0,1516806600.0,1516810500.0,4154,65,9.263e-05,9.309431077694235e-05,false,"roi"],["ADA/BTC",-0.0,1516833600.0,1516836300.0,4244,45,5.514e-05,5.5416390977443596e-05,false,"roi"],["XLM/BTC",0.0,1516841400.0,1516843800.0,4270,40,4.921e-05,4.9456666666666664e-05,false,"roi"],["ETC/BTC",0.0,1516868100.0,1516882500.0,4359,240,0.0026,0.002613032581453634,false,"roi"],["XMR/BTC",-0.0,1516875900.0,1516896900.0,4385,350,0.02799871,0.028139054411027563,false,"roi"],["ZEC/BTC",-0.0,1516878000.0,1516880700.0,4392,45,0.04078902,0.0409934762406015,false,"roi"],["NXT/BTC",-0.0,1516885500.0,1516887900.0,4417,40,2.89e-05,2.904486215538847e-05,false,"roi"],["ZEC/BTC",-0.0,1516886400.0,1516889100.0,4420,45,0.041103,0.041309030075187964,false,"roi"],["XLM/BTC",0.00997506,1516895100.0,1516896900.0,4449,30,5.428e-05,5.5096240601503756e-05,false,"roi"],["XLM/BTC",-0.0,1516902300.0,1516922100.0,4473,330,5.414e-05,5.441137844611528e-05,false,"roi"],["ZEC/BTC",-0.0,1516914900.0,1516917300.0,4515,40,0.04140777,0.0416153277443609,false,"roi"],["ETC/BTC",0.0,1516932300.0,1516934700.0,4573,40,0.00254309,0.002555837318295739,false,"roi"],["ADA/BTC",-0.0,1516935300.0,1516979400.0,4583,735,5.607e-05,5.6351052631578935e-05,false,"roi"],["ETC/BTC",0.0,1516947000.0,1516958700.0,4622,195,0.00253806,0.0025507821052631577,false,"roi"],["ZEC/BTC",-0.0,1516951500.0,1516960500.0,4637,150,0.0415,0.04170802005012531,false,"roi"],["XLM/BTC",0.00997506,1516960500.0,1516962300.0,4667,30,5.321e-05,5.401015037593984e-05,false,"roi"],["XMR/BTC",-0.0,1516982700.0,1516985100.0,4741,40,0.02772046,0.02785940967418546,false,"roi"],["ETH/BTC",0.0,1517009700.0,1517012100.0,4831,40,0.09461341,0.09508766268170425,false,"roi"],["XLM/BTC",-0.0,1517013300.0,1517016600.0,4843,55,5.615e-05,5.643145363408521e-05,false,"roi"],["ADA/BTC",-0.07877175,1517013900.0,1517287500.0,4845,4560,5.556e-05,5.144e-05,true,"force_sell"],["DASH/BTC",-0.0,1517020200.0,1517052300.0,4866,535,0.06900001,0.06934587471177944,false,"roi"],["ETH/BTC",-0.0,1517034300.0,1517036700.0,4913,40,0.09449985,0.09497353345864659,false,"roi"],["ZEC/BTC",-0.04815133,1517046000.0,1517287200.0,4952,4020,0.0410697,0.03928809,true,"force_sell"],["XMR/BTC",-0.0,1517053500.0,1517056200.0,4977,45,0.0285,0.02864285714285714,false,"roi"],["XMR/BTC",-0.0,1517056500.0,1517066700.0,4987,170,0.02866372,0.02880739779448621,false,"roi"],["ETH/BTC",-0.0,1517068200.0,1517071800.0,5026,60,0.095381,0.09585910025062655,false,"roi"],["DASH/BTC",-0.0,1517072700.0,1517075100.0,5041,40,0.06759092,0.06792972160401002,false,"roi"],["ETC/BTC",-0.0,1517096400.0,1517101500.0,5120,85,0.00258501,0.002597967443609022,false,"roi"],["DASH/BTC",-0.0,1517106300.0,1517127000.0,5153,345,0.06698502,0.0673207845112782,false,"roi"],["DASH/BTC",-0.0,1517135100.0,1517157000.0,5249,365,0.0677177,0.06805713709273183,false,"roi"],["XLM/BTC",0.0,1517171700.0,1517175300.0,5371,60,5.215e-05,5.2411403508771925e-05,false,"roi"],["ETC/BTC",0.00997506,1517176800.0,1517178600.0,5388,30,0.00273809,0.002779264285714285,false,"roi"],["ETC/BTC",0.00997506,1517184000.0,1517185800.0,5412,30,0.00274632,0.002787618045112782,false,"roi"],["LTC/BTC",0.0,1517192100.0,1517194800.0,5439,45,0.01622478,0.016306107218045113,false,"roi"],["DASH/BTC",-0.0,1517195100.0,1517197500.0,5449,40,0.069,0.06934586466165413,false,"roi"],["POWR/BTC",-0.0,1517203200.0,1517208900.0,5476,95,8.755e-05,8.798884711779448e-05,false,"roi"],["DASH/BTC",-0.0,1517209200.0,1517253900.0,5496,745,0.06825763,0.06859977350877192,false,"roi"],["DASH/BTC",-0.0,1517255100.0,1517257500.0,5649,40,0.06713892,0.06747545593984962,false,"roi"],["POWR/BTC",-0.0199116,1517268600.0,1517287500.0,5694,315,8.934e-05,8.8e-05,true,"force_sell"]] \ No newline at end of file +[["TRX/BTC",0.03990025,1515568500.0,1515568800.0,27,5,9.64e-05,0.00010074887218045112,false,"roi"],["ADA/BTC",0.03990025,1515568500.0,1515569400.0,27,15,4.756e-05,4.9705563909774425e-05,false,"roi"],["XLM/BTC",0.03990025,1515569100.0,1515569700.0,29,10,3.339e-05,3.489631578947368e-05,false,"roi"],["TRX/BTC",0.03990025,1515569100.0,1515570000.0,29,15,9.696e-05,0.00010133413533834584,false,"roi"],["ETH/BTC",-0.0,1515569700.0,1515573300.0,31,60,0.0943,0.09477268170426063,false,"roi"],["XMR/BTC",0.00997506,1515570000.0,1515571800.0,32,30,0.02719607,0.02760503345864661,false,"roi"],["ZEC/BTC",0.0,1515572100.0,1515578100.0,39,100,0.04634952,0.046581848421052625,false,"roi"],["NXT/BTC",-0.0,1515595500.0,1515599400.0,117,65,3.066e-05,3.081368421052631e-05,false,"roi"],["LTC/BTC",0.0,1515602100.0,1515604500.0,139,40,0.0168999,0.016984611278195488,false,"roi"],["ETH/BTC",-0.0,1515602400.0,1515604800.0,140,40,0.09132568,0.0917834528320802,false,"roi"],["ETH/BTC",-0.0,1515610200.0,1515613500.0,166,55,0.08898003,0.08942604518796991,false,"roi"],["ETH/BTC",0.0,1515622500.0,1515625200.0,207,45,0.08560008,0.08602915308270676,false,"roi"],["ETC/BTC",0.00997506,1515624600.0,1515626400.0,214,30,0.00249083,0.0025282860902255634,false,"roi"],["NXT/BTC",-0.0,1515626100.0,1515629700.0,219,60,3.022e-05,3.037147869674185e-05,false,"roi"],["ETC/BTC",0.01995012,1515627600.0,1515629100.0,224,25,0.002437,0.0024980776942355883,false,"roi"],["ZEC/BTC",0.00997506,1515628800.0,1515630900.0,228,35,0.04771803,0.04843559436090225,false,"roi"],["XLM/BTC",-0.10448878,1515642000.0,1515644700.0,272,45,3.651e-05,3.2859000000000005e-05,false,"stop_loss"],["ETH/BTC",0.00997506,1515642900.0,1515644700.0,275,30,0.08824105,0.08956798308270676,false,"roi"],["ETC/BTC",-0.0,1515643200.0,1515646200.0,276,50,0.00243,0.002442180451127819,false,"roi"],["ZEC/BTC",0.01995012,1515645000.0,1515646500.0,282,25,0.04545064,0.046589753784461146,false,"roi"],["XLM/BTC",0.01995012,1515645000.0,1515646200.0,282,20,3.372e-05,3.456511278195488e-05,false,"roi"],["XMR/BTC",0.01995012,1515646500.0,1515647700.0,287,20,0.02644,0.02710265664160401,false,"roi"],["ETH/BTC",-0.0,1515669600.0,1515672000.0,364,40,0.08812,0.08856170426065162,false,"roi"],["XMR/BTC",-0.0,1515670500.0,1515672900.0,367,40,0.02683577,0.026970285137844607,false,"roi"],["ADA/BTC",0.01995012,1515679200.0,1515680700.0,396,25,4.919e-05,5.04228320802005e-05,false,"roi"],["ETH/BTC",-0.0,1515698700.0,1515702900.0,461,70,0.08784896,0.08828930566416039,false,"roi"],["ADA/BTC",-0.0,1515710100.0,1515713400.0,499,55,5.105e-05,5.130588972431077e-05,false,"roi"],["XLM/BTC",0.00997506,1515711300.0,1515713100.0,503,30,3.96e-05,4.019548872180451e-05,false,"roi"],["NXT/BTC",-0.0,1515711300.0,1515713700.0,503,40,2.885e-05,2.899461152882205e-05,false,"roi"],["XMR/BTC",0.00997506,1515713400.0,1515715500.0,510,35,0.02645,0.026847744360902256,false,"roi"],["ZEC/BTC",-0.0,1515714900.0,1515719700.0,515,80,0.048,0.04824060150375939,false,"roi"],["XLM/BTC",0.01995012,1515791700.0,1515793200.0,771,25,4.692e-05,4.809593984962405e-05,false,"roi"],["ETC/BTC",-0.0,1515804900.0,1515824400.0,815,325,0.00256966,0.0025825405012531327,false,"roi"],["ADA/BTC",0.0,1515840900.0,1515843300.0,935,40,6.262e-05,6.293388471177944e-05,false,"roi"],["XLM/BTC",0.0,1515848700.0,1516025400.0,961,2945,4.73e-05,4.753709273182957e-05,false,"roi"],["ADA/BTC",-0.0,1515850200.0,1515854700.0,966,75,6.063e-05,6.0933909774436085e-05,false,"roi"],["TRX/BTC",-0.0,1515850800.0,1515886200.0,968,590,0.00011082,0.00011137548872180449,false,"roi"],["ADA/BTC",-0.0,1515856500.0,1515858900.0,987,40,5.93e-05,5.9597243107769415e-05,false,"roi"],["ZEC/BTC",-0.0,1515861000.0,1515863400.0,1002,40,0.04850003,0.04874313791979949,false,"roi"],["ETH/BTC",-0.0,1515881100.0,1515911100.0,1069,500,0.09825019,0.09874267215538847,false,"roi"],["ADA/BTC",0.0,1515889200.0,1515970500.0,1096,1355,6.018e-05,6.048165413533834e-05,false,"roi"],["ETH/BTC",-0.0,1515933900.0,1515936300.0,1245,40,0.09758999,0.0980791628822055,false,"roi"],["ETC/BTC",0.00997506,1515943800.0,1515945600.0,1278,30,0.00311,0.0031567669172932328,false,"roi"],["ETC/BTC",-0.0,1515962700.0,1515968100.0,1341,90,0.00312401,0.003139669197994987,false,"roi"],["LTC/BTC",0.0,1515972900.0,1515976200.0,1375,55,0.0174679,0.017555458395989976,false,"roi"],["DASH/BTC",-0.0,1515973500.0,1515975900.0,1377,40,0.07346846,0.07383672295739348,false,"roi"],["ETH/BTC",-0.0,1515983100.0,1515985500.0,1409,40,0.097994,0.09848519799498745,false,"roi"],["ETH/BTC",-0.0,1516000800.0,1516003200.0,1468,40,0.09659,0.09707416040100249,false,"roi"],["TRX/BTC",0.00997506,1516004400.0,1516006500.0,1480,35,9.987e-05,0.00010137180451127818,false,"roi"],["ETH/BTC",0.0,1516018200.0,1516071000.0,1526,880,0.0948969,0.09537257368421052,false,"roi"],["DASH/BTC",-0.0,1516025400.0,1516038000.0,1550,210,0.071,0.07135588972431077,false,"roi"],["ZEC/BTC",-0.0,1516026600.0,1516029000.0,1554,40,0.04600501,0.046235611553884705,false,"roi"],["TRX/BTC",-0.0,1516039800.0,1516044300.0,1598,75,9.438e-05,9.485308270676691e-05,false,"roi"],["XMR/BTC",-0.0,1516041300.0,1516043700.0,1603,40,0.03040001,0.030552391002506264,false,"roi"],["ADA/BTC",-0.10448878,1516047900.0,1516091100.0,1625,720,5.837e-05,5.2533e-05,false,"stop_loss"],["ZEC/BTC",-0.0,1516048800.0,1516053600.0,1628,80,0.046036,0.04626675689223057,false,"roi"],["ETC/BTC",-0.0,1516062600.0,1516065000.0,1674,40,0.0028685,0.0028828784461152877,false,"roi"],["DASH/BTC",0.0,1516065300.0,1516070100.0,1683,80,0.06731755,0.0676549813283208,false,"roi"],["ETH/BTC",0.0,1516088700.0,1516092000.0,1761,55,0.09217614,0.09263817578947368,false,"roi"],["LTC/BTC",0.01995012,1516091700.0,1516092900.0,1771,20,0.0165,0.016913533834586467,false,"roi"],["TRX/BTC",0.03990025,1516091700.0,1516092000.0,1771,5,7.953e-05,8.311781954887218e-05,false,"roi"],["ZEC/BTC",-0.0,1516092300.0,1516096200.0,1773,65,0.045202,0.04542857644110275,false,"roi"],["ADA/BTC",0.00997506,1516094100.0,1516095900.0,1779,30,5.248e-05,5.326917293233082e-05,false,"roi"],["XMR/BTC",0.0,1516094100.0,1516096500.0,1779,40,0.02892318,0.02906815834586466,false,"roi"],["ADA/BTC",0.01995012,1516096200.0,1516097400.0,1786,20,5.158e-05,5.287273182957392e-05,false,"roi"],["ZEC/BTC",0.00997506,1516097100.0,1516099200.0,1789,35,0.04357584,0.044231115789473675,false,"roi"],["XMR/BTC",0.00997506,1516097100.0,1516098900.0,1789,30,0.02828232,0.02870761804511278,false,"roi"],["ADA/BTC",0.00997506,1516110300.0,1516112400.0,1833,35,5.362e-05,5.4426315789473676e-05,false,"roi"],["ADA/BTC",-0.0,1516123800.0,1516127100.0,1878,55,5.302e-05,5.328576441102756e-05,false,"roi"],["ETH/BTC",0.00997506,1516126500.0,1516128300.0,1887,30,0.09129999,0.09267292218045112,false,"roi"],["XLM/BTC",0.01995012,1516126500.0,1516127700.0,1887,20,3.808e-05,3.903438596491228e-05,false,"roi"],["XMR/BTC",0.00997506,1516129200.0,1516131000.0,1896,30,0.02811012,0.028532828571428567,false,"roi"],["ETC/BTC",-0.10448878,1516137900.0,1516141500.0,1925,60,0.00258379,0.002325411,false,"stop_loss"],["NXT/BTC",-0.10448878,1516137900.0,1516142700.0,1925,80,2.559e-05,2.3031e-05,false,"stop_loss"],["TRX/BTC",-0.10448878,1516138500.0,1516141500.0,1927,50,7.62e-05,6.858e-05,false,"stop_loss"],["LTC/BTC",0.03990025,1516141800.0,1516142400.0,1938,10,0.0151,0.015781203007518795,false,"roi"],["ETC/BTC",0.03990025,1516141800.0,1516142100.0,1938,5,0.00229844,0.002402129022556391,false,"roi"],["ETC/BTC",0.03990025,1516142400.0,1516142700.0,1940,5,0.00235676,0.00246308,false,"roi"],["DASH/BTC",0.01995012,1516142700.0,1516143900.0,1941,20,0.0630692,0.06464988170426066,false,"roi"],["NXT/BTC",0.03990025,1516143000.0,1516143300.0,1942,5,2.2e-05,2.2992481203007514e-05,false,"roi"],["ADA/BTC",0.00997506,1516159800.0,1516161600.0,1998,30,4.974e-05,5.048796992481203e-05,false,"roi"],["TRX/BTC",0.01995012,1516161300.0,1516162500.0,2003,20,7.108e-05,7.28614536340852e-05,false,"roi"],["ZEC/BTC",-0.0,1516181700.0,1516184100.0,2071,40,0.04327,0.04348689223057644,false,"roi"],["ADA/BTC",-0.0,1516184400.0,1516208400.0,2080,400,4.997e-05,5.022047619047618e-05,false,"roi"],["DASH/BTC",-0.0,1516185000.0,1516188300.0,2082,55,0.06836818,0.06871087764411027,false,"roi"],["XLM/BTC",-0.0,1516185000.0,1516187400.0,2082,40,3.63e-05,3.648195488721804e-05,false,"roi"],["XMR/BTC",-0.0,1516192200.0,1516226700.0,2106,575,0.0281,0.02824085213032581,false,"roi"],["ETH/BTC",-0.0,1516192500.0,1516208100.0,2107,260,0.08651001,0.08694364413533832,false,"roi"],["ADA/BTC",-0.0,1516251600.0,1516254900.0,2304,55,5.633e-05,5.6612355889724306e-05,false,"roi"],["DASH/BTC",0.00997506,1516252800.0,1516254900.0,2308,35,0.06988494,0.07093584135338346,false,"roi"],["ADA/BTC",-0.0,1516260900.0,1516263300.0,2335,40,5.545e-05,5.572794486215538e-05,false,"roi"],["LTC/BTC",-0.0,1516266000.0,1516268400.0,2352,40,0.01633527,0.016417151052631574,false,"roi"],["ETC/BTC",-0.0,1516293600.0,1516296000.0,2444,40,0.00269734,0.0027108605012531326,false,"roi"],["XLM/BTC",0.01995012,1516298700.0,1516300200.0,2461,25,4.475e-05,4.587155388471177e-05,false,"roi"],["NXT/BTC",0.00997506,1516299900.0,1516301700.0,2465,30,2.79e-05,2.8319548872180444e-05,false,"roi"],["ZEC/BTC",0.0,1516306200.0,1516308600.0,2486,40,0.04439326,0.04461578260651629,false,"roi"],["XLM/BTC",0.0,1516311000.0,1516322100.0,2502,185,4.49e-05,4.51250626566416e-05,false,"roi"],["XMR/BTC",-0.0,1516312500.0,1516338300.0,2507,430,0.02855,0.028693107769423555,false,"roi"],["ADA/BTC",0.0,1516313400.0,1516315800.0,2510,40,5.796e-05,5.8250526315789473e-05,false,"roi"],["ZEC/BTC",0.0,1516319400.0,1516321800.0,2530,40,0.04340323,0.04362079005012531,false,"roi"],["ZEC/BTC",0.0,1516380300.0,1516383300.0,2733,50,0.04454455,0.04476783095238095,false,"roi"],["ADA/BTC",-0.0,1516382100.0,1516391700.0,2739,160,5.62e-05,5.648170426065162e-05,false,"roi"],["XLM/BTC",-0.0,1516382400.0,1516392900.0,2740,175,4.339e-05,4.360749373433584e-05,false,"roi"],["TRX/BTC",0.0,1516423500.0,1516469700.0,2877,770,0.0001009,0.00010140576441102757,false,"roi"],["ETC/BTC",-0.0,1516423800.0,1516461300.0,2878,625,0.00270505,0.002718609147869674,false,"roi"],["XMR/BTC",-0.0,1516423800.0,1516431600.0,2878,130,0.03000002,0.030150396040100245,false,"roi"],["ADA/BTC",-0.0,1516438800.0,1516441200.0,2928,40,5.46e-05,5.4873684210526304e-05,false,"roi"],["XMR/BTC",-0.10448878,1516472700.0,1516852200.0,3041,6325,0.03082222,0.027739998000000002,false,"stop_loss"],["ETH/BTC",-0.0,1516487100.0,1516490100.0,3089,50,0.08969999,0.09014961401002504,false,"roi"],["LTC/BTC",0.0,1516503000.0,1516545000.0,3142,700,0.01632501,0.01640683962406015,false,"roi"],["DASH/BTC",-0.0,1516530000.0,1516532400.0,3232,40,0.070538,0.07089157393483708,false,"roi"],["ADA/BTC",-0.0,1516549800.0,1516560300.0,3298,175,5.301e-05,5.3275714285714276e-05,false,"roi"],["XLM/BTC",0.0,1516551600.0,1516554000.0,3304,40,3.955e-05,3.9748245614035085e-05,false,"roi"],["ETC/BTC",0.00997506,1516569300.0,1516571100.0,3363,30,0.00258505,0.002623922932330827,false,"roi"],["XLM/BTC",-0.0,1516569300.0,1516571700.0,3363,40,3.903e-05,3.922563909774435e-05,false,"roi"],["ADA/BTC",-0.0,1516581300.0,1516617300.0,3403,600,5.236e-05,5.262245614035087e-05,false,"roi"],["TRX/BTC",0.0,1516584600.0,1516587000.0,3414,40,9.028e-05,9.073253132832079e-05,false,"roi"],["ETC/BTC",-0.0,1516623900.0,1516631700.0,3545,130,0.002687,0.002700468671679198,false,"roi"],["XLM/BTC",-0.0,1516626900.0,1516629300.0,3555,40,4.168e-05,4.1888922305764405e-05,false,"roi"],["TRX/BTC",0.00997506,1516629600.0,1516631400.0,3564,30,8.821e-05,8.953646616541353e-05,false,"roi"],["ADA/BTC",-0.0,1516636500.0,1516639200.0,3587,45,5.172e-05,5.1979248120300745e-05,false,"roi"],["NXT/BTC",0.01995012,1516637100.0,1516638300.0,3589,20,3.026e-05,3.101839598997494e-05,false,"roi"],["DASH/BTC",0.0,1516650600.0,1516666200.0,3634,260,0.07064,0.07099408521303258,false,"roi"],["LTC/BTC",0.0,1516656300.0,1516658700.0,3653,40,0.01644483,0.01652726022556391,false,"roi"],["XLM/BTC",0.00997506,1516665900.0,1516667700.0,3685,30,4.331e-05,4.3961278195488714e-05,false,"roi"],["NXT/BTC",0.01995012,1516672200.0,1516673700.0,3706,25,3.2e-05,3.2802005012531326e-05,false,"roi"],["ETH/BTC",0.0,1516681500.0,1516684500.0,3737,50,0.09167706,0.09213659413533835,false,"roi"],["DASH/BTC",0.0,1516692900.0,1516698000.0,3775,85,0.0692498,0.06959691679197995,false,"roi"],["NXT/BTC",0.0,1516704600.0,1516712700.0,3814,135,3.182e-05,3.197949874686716e-05,false,"roi"],["ZEC/BTC",-0.0,1516705500.0,1516723500.0,3817,300,0.04088,0.04108491228070175,false,"roi"],["ADA/BTC",-0.0,1516719300.0,1516721700.0,3863,40,5.15e-05,5.175814536340851e-05,false,"roi"],["ETH/BTC",0.0,1516725300.0,1516752300.0,3883,450,0.09071698,0.09117170170426064,false,"roi"],["NXT/BTC",-0.0,1516728300.0,1516733100.0,3893,80,3.128e-05,3.1436791979949865e-05,false,"roi"],["TRX/BTC",-0.0,1516738500.0,1516744800.0,3927,105,9.555e-05,9.602894736842104e-05,false,"roi"],["ZEC/BTC",-0.0,1516746600.0,1516749000.0,3954,40,0.04080001,0.041004521328320796,false,"roi"],["ADA/BTC",-0.0,1516751400.0,1516764900.0,3970,225,5.163e-05,5.1888796992481196e-05,false,"roi"],["ZEC/BTC",0.0,1516753200.0,1516758600.0,3976,90,0.04040781,0.04061035541353383,false,"roi"],["ADA/BTC",-0.0,1516776300.0,1516778700.0,4053,40,5.132e-05,5.157724310776942e-05,false,"roi"],["ADA/BTC",0.03990025,1516803300.0,1516803900.0,4143,10,5.198e-05,5.432496240601503e-05,false,"roi"],["NXT/BTC",-0.0,1516805400.0,1516811700.0,4150,105,3.054e-05,3.069308270676692e-05,false,"roi"],["TRX/BTC",0.0,1516806600.0,1516810500.0,4154,65,9.263e-05,9.309431077694235e-05,false,"roi"],["ADA/BTC",-0.0,1516833600.0,1516836300.0,4244,45,5.514e-05,5.5416390977443596e-05,false,"roi"],["XLM/BTC",0.0,1516841400.0,1516843800.0,4270,40,4.921e-05,4.9456666666666664e-05,false,"roi"],["ETC/BTC",0.0,1516868100.0,1516882500.0,4359,240,0.0026,0.002613032581453634,false,"roi"],["XMR/BTC",-0.0,1516875900.0,1516896900.0,4385,350,0.02799871,0.028139054411027563,false,"roi"],["ZEC/BTC",-0.0,1516878000.0,1516880700.0,4392,45,0.04078902,0.0409934762406015,false,"roi"],["NXT/BTC",-0.0,1516885500.0,1516887900.0,4417,40,2.89e-05,2.904486215538847e-05,false,"roi"],["ZEC/BTC",-0.0,1516886400.0,1516889100.0,4420,45,0.041103,0.041309030075187964,false,"roi"],["XLM/BTC",0.00997506,1516895100.0,1516896900.0,4449,30,5.428e-05,5.5096240601503756e-05,false,"roi"],["XLM/BTC",-0.0,1516902300.0,1516922100.0,4473,330,5.414e-05,5.441137844611528e-05,false,"roi"],["ZEC/BTC",-0.0,1516914900.0,1516917300.0,4515,40,0.04140777,0.0416153277443609,false,"roi"],["ETC/BTC",0.0,1516932300.0,1516934700.0,4573,40,0.00254309,0.002555837318295739,false,"roi"],["ADA/BTC",-0.0,1516935300.0,1516979400.0,4583,735,5.607e-05,5.6351052631578935e-05,false,"roi"],["ETC/BTC",0.0,1516947000.0,1516958700.0,4622,195,0.00253806,0.0025507821052631577,false,"roi"],["ZEC/BTC",-0.0,1516951500.0,1516960500.0,4637,150,0.0415,0.04170802005012531,false,"roi"],["XLM/BTC",0.00997506,1516960500.0,1516962300.0,4667,30,5.321e-05,5.401015037593984e-05,false,"roi"],["XMR/BTC",-0.0,1516982700.0,1516985100.0,4741,40,0.02772046,0.02785940967418546,false,"roi"],["ETH/BTC",0.0,1517009700.0,1517012100.0,4831,40,0.09461341,0.09508766268170425,false,"roi"],["XLM/BTC",-0.0,1517013300.0,1517016600.0,4843,55,5.615e-05,5.643145363408521e-05,false,"roi"],["ADA/BTC",-0.07877175,1517013900.0,1517287500.0,4845,4560,5.556e-05,5.144e-05,true,"force_sell"],["DASH/BTC",-0.0,1517020200.0,1517052300.0,4866,535,0.06900001,0.06934587471177944,false,"roi"],["ETH/BTC",-0.0,1517034300.0,1517036700.0,4913,40,0.09449985,0.09497353345864659,false,"roi"],["ZEC/BTC",-0.04815133,1517046000.0,1517287200.0,4952,4020,0.0410697,0.03928809,true,"force_sell"],["XMR/BTC",-0.0,1517053500.0,1517056200.0,4977,45,0.0285,0.02864285714285714,false,"roi"],["XMR/BTC",-0.0,1517056500.0,1517066700.0,4987,170,0.02866372,0.02880739779448621,false,"roi"],["ETH/BTC",-0.0,1517068200.0,1517071800.0,5026,60,0.095381,0.09585910025062655,false,"roi"],["DASH/BTC",-0.0,1517072700.0,1517075100.0,5041,40,0.06759092,0.06792972160401002,false,"roi"],["ETC/BTC",-0.0,1517096400.0,1517101500.0,5120,85,0.00258501,0.002597967443609022,false,"roi"],["DASH/BTC",-0.0,1517106300.0,1517127000.0,5153,345,0.06698502,0.0673207845112782,false,"roi"],["DASH/BTC",-0.0,1517135100.0,1517157000.0,5249,365,0.0677177,0.06805713709273183,false,"roi"],["XLM/BTC",0.0,1517171700.0,1517175300.0,5371,60,5.215e-05,5.2411403508771925e-05,false,"roi"],["ETC/BTC",0.00997506,1517176800.0,1517178600.0,5388,30,0.00273809,0.002779264285714285,false,"roi"],["ETC/BTC",0.00997506,1517184000.0,1517185800.0,5412,30,0.00274632,0.002787618045112782,false,"roi"],["LTC/BTC",0.0,1517192100.0,1517194800.0,5439,45,0.01622478,0.016306107218045113,false,"roi"],["DASH/BTC",-0.0,1517195100.0,1517197500.0,5449,40,0.069,0.06934586466165413,false,"roi"],["TRX/BTC",-0.0,1517203200.0,1517208900.0,5476,95,8.755e-05,8.798884711779448e-05,false,"roi"],["DASH/BTC",-0.0,1517209200.0,1517253900.0,5496,745,0.06825763,0.06859977350877192,false,"roi"],["DASH/BTC",-0.0,1517255100.0,1517257500.0,5649,40,0.06713892,0.06747545593984962,false,"roi"],["TRX/BTC",-0.0199116,1517268600.0,1517287500.0,5694,315,8.934e-05,8.8e-05,true,"force_sell"]] diff --git a/tests/testdata/pairs.json b/tests/testdata/pairs.json index f4bab6dc5..15aae2643 100644 --- a/tests/testdata/pairs.json +++ b/tests/testdata/pairs.json @@ -9,7 +9,7 @@ "LTC/BTC", "NEO/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "STORJ/BTC", "QTUM/BTC", "WAVES/BTC", From cff8498b4227f9c0f4688eb158a348b2067de677 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 Jan 2020 20:17:53 +0100 Subject: [PATCH 008/563] Version bump 2020.01 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index bab359368..15b7d66d7 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.11' +__version__ = '2020.01' if __version__ == 'develop': From a6b48f7366c749693fce9144266bb6a74ffdc2d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Feb 2020 15:16:55 +0100 Subject: [PATCH 009/563] Version bump 2020.02 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 1a7cb087e..bb1321237 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.01' +__version__ = '2020.02' if __version__ == 'develop': From da191e4ac8a1cf0f1170c38ff2d2736c45069a94 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Mar 2020 17:45:44 +0100 Subject: [PATCH 010/563] Version bump 2020.3 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index bb1321237..080fde242 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.02' +__version__ = '2020.3' if __version__ == 'develop': From fa6fe618acec2a752dc334e5acf7e887250fb7e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Apr 2020 07:58:31 +0200 Subject: [PATCH 011/563] Version bump 2020.4 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 048c92a84..647682d70 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.3' +__version__ = '2020.4' if __version__ == 'develop': From 95780cb4a1c28720a69dcc07806b3881bbda354d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 May 2020 19:41:53 +0200 Subject: [PATCH 012/563] Version bump to 2020.5 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 647682d70..602aece94 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.4' +__version__ = '2020.5' if __version__ == 'develop': From 6f870e980daa1bb40e27ad6b36d9c59141927a94 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 May 2020 10:55:00 +0200 Subject: [PATCH 013/563] Version bump Pandas to 1.0.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 18cab206b..f5d09db4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.18.4 -pandas==1.0.3 +pandas==1.0.4 From e41392a73f1c019cce70b959ba4990f10b533419 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Jun 2020 06:44:40 +0200 Subject: [PATCH 014/563] Version bump to 2020.6 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 602aece94..1eb0f9bec 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.5' +__version__ = '2020.6' if __version__ == 'develop': From 7263f83f78612d94497b201fae9ce1411e1965ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Jul 2020 19:53:05 +0200 Subject: [PATCH 015/563] Version bump 2020.7 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 1eb0f9bec..7d1ef43ad 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.6' +__version__ = '2020.7' if __version__ == 'develop': From 77f2d46e296aee8c3a6954dc48a74471401f032d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Aug 2020 10:11:57 +0200 Subject: [PATCH 016/563] Version bump to 2020.8 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 7d1ef43ad..dd7009cb2 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.7' +__version__ = '2020.8' if __version__ == 'develop': From 17659001d8bf09cd290bec438fed6b3dbbba10b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 09:51:19 +0200 Subject: [PATCH 017/563] Version bump to 2020.9 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index dd7009cb2..4f7825cbd 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.8' +__version__ = '2020.9' if __version__ == 'develop': From 44e81845192168269b8d10781dcb15841f934905 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 08:36:40 +0200 Subject: [PATCH 018/563] Tag image before building next image --- build_helpers/publish_docker.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 53e18063c..ac0cd2461 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -17,8 +17,13 @@ else docker pull ${IMAGE_NAME}:${TAG} docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . fi +# Tag image for upload and next build step +docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG + docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . +docker tag freqtrade:$TAG_PLOT ${IMAGE_NAME}:$TAG_PLOT + if [ $? -ne 0 ]; then echo "failed building image" return 1 @@ -32,9 +37,6 @@ if [ $? -ne 0 ]; then return 1 fi -# Tag image for upload -docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG -docker tag freqtrade:$TAG_PLOT ${IMAGE_NAME}:$TAG_PLOT if [ $? -ne 0 ]; then echo "failed tagging image" return 1 From 1f14c6bacdbe8377ef2d029a4b10198253cc5224 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 08:53:53 +0200 Subject: [PATCH 019/563] PyPi Publis should only run for releases --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 392641677..f4ef3ba7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -236,7 +236,7 @@ jobs: - name: Publish to PyPI (Test) uses: pypa/gh-action-pypi-publish@master - if: (steps.extract_branch.outputs.branch == 'stable' || github.event_name == 'release') + if: (github.event_name == 'release') with: user: __token__ password: ${{ secrets.pypi_test_password }} @@ -244,7 +244,7 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@master - if: (steps.extract_branch.outputs.branch == 'stable' || github.event_name == 'release') + if: (github.event_name == 'release') with: user: __token__ password: ${{ secrets.pypi_password }} From 98a2811605efb7fd1e570cfb8e6bf54d59ed623b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 08:55:17 +0200 Subject: [PATCH 020/563] Version bump 2020.9.1 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 4f7825cbd..f1b88805b 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.9' +__version__ = '2020.9.1' if __version__ == 'develop': From aaa6468983d67e48f49c2df5bcce98b7826f46a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Oct 2020 07:48:05 +0100 Subject: [PATCH 021/563] Version bump to 2020.10 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index f1b88805b..125dca1ef 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.9.1' +__version__ = '2020.10' if __version__ == 'develop': From 1353c59f188d49a3f317bac853bd2ae4e3a08b34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 11:19:01 +0100 Subject: [PATCH 022/563] Version bump to 2020.11 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 125dca1ef..3054bc4a1 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.10' +__version__ = '2020.11' if __version__ == 'develop': From 768a24c375014f755fa857c4b09c8bf4e0154922 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Dec 2020 07:45:41 +0100 Subject: [PATCH 023/563] Add stoplossvalue interface --- docs/strategy-advanced.md | 5 +++++ freqtrade/strategy/interface.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 359280694..85a5a6bc6 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -8,6 +8,11 @@ If you're just getting started, please be familiar with the methods described in !!! Note All callback methods described below should only be implemented in a strategy if they are actually used. +## Custom stoploss logic + +// TODO: Complete this section + + ## Custom order timeout rules Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 027c5d36e..33a7ef0c1 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -254,6 +254,24 @@ class IStrategy(ABC): """ return True + def stoploss_value(self, pair: str, trade: Trade, rate: float, **kwargs) -> float: + """ + Define custom stoploss logic + The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. + + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns the initial stoploss value + + :param pair: Pair that's about to be sold. + :param trade: trade object. + :param rate: Rate that's going to be used when using limit orders + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: New stoploss value, relative to the open price + """ + return self.stoploss + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. From 8f6aefb591f4d69e8a023c935e3994aab5e3b60c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Dec 2020 07:41:06 +0100 Subject: [PATCH 024/563] Extract stoploss assignment --- freqtrade/persistence/models.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7fa894e9c..e803b4383 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -342,6 +342,12 @@ class Trade(_DECL_BASE): self.max_rate = max(current_price, self.max_rate or self.open_rate) self.min_rate = min(current_price, self.min_rate or self.open_rate) + def _set_new_stoploss(self, new_loss: float, stoploss: float): + """Assign new stop value""" + self.stop_loss = new_loss + self.stop_loss_pct = -1 * abs(stoploss) + self.stoploss_last_update = datetime.utcnow() + def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False) -> None: """ @@ -360,19 +366,15 @@ class Trade(_DECL_BASE): # no stop loss assigned yet if not self.stop_loss: logger.debug(f"{self.pair} - Assigning new stoploss...") - self.stop_loss = new_loss - self.stop_loss_pct = -1 * abs(stoploss) + self._set_new_stoploss(new_loss, stoploss) self.initial_stop_loss = new_loss self.initial_stop_loss_pct = -1 * abs(stoploss) - self.stoploss_last_update = datetime.utcnow() # evaluate if the stop loss needs to be updated else: if new_loss > self.stop_loss: # stop losses only walk up, never down! logger.debug(f"{self.pair} - Adjusting stoploss...") - self.stop_loss = new_loss - self.stop_loss_pct = -1 * abs(stoploss) - self.stoploss_last_update = datetime.utcnow() + self._set_new_stoploss(new_loss, stoploss) else: logger.debug(f"{self.pair} - Keeping current stoploss...") From a414b57d5469402f4002d7affbf69fedb61b9f11 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Dec 2020 07:06:16 +0100 Subject: [PATCH 025/563] Experiment with custom stoploss interface --- freqtrade/strategy/interface.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 33a7ef0c1..7f60ba62f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -89,6 +89,7 @@ class IStrategy(ABC): trailing_stop_positive: Optional[float] = None trailing_stop_positive_offset: float = 0.0 trailing_only_offset_is_reached = False + custom_stoploss: bool = False # associated timeframe ticker_interval: str # DEPRECATED @@ -254,21 +255,22 @@ class IStrategy(ABC): """ return True - def stoploss_value(self, pair: str, trade: Trade, rate: float, **kwargs) -> float: + def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, + **kwargs) -> float: """ Define custom stoploss logic The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. - For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, returns the initial stoploss value :param pair: Pair that's about to be sold. :param trade: trade object. - :param rate: Rate that's going to be used when using limit orders + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New stoploss value, relative to the open price + :return float: New stoploss value, relative to the currentrate """ return self.stoploss @@ -549,6 +551,18 @@ class IStrategy(ABC): # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) + if self.custom_stoploss: + stop_loss_value = strategy_safe_wrapper(self.stoploss_value, default_retval=None + )(pair=trade.pair, trade=trade, + current_rate=current_rate, + current_profit=current_profit) + # Sanity check - error cases will return None + if stop_loss_value: + # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") + trade.adjust_stop_loss(current_rate, stop_loss_value) + else: + logger.warning("CustomStoploss function did not return valid stoploss") + if self.trailing_stop: # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset From 18795844d818e983cdda0041799659dad7a7f963 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Dec 2020 07:59:15 +0100 Subject: [PATCH 026/563] Add initial set of custom stoploss documentation --- docs/strategy-advanced.md | 48 +++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 85a5a6bc6..d4068c82b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -8,16 +8,56 @@ If you're just getting started, please be familiar with the methods described in !!! Note All callback methods described below should only be implemented in a strategy if they are actually used. -## Custom stoploss logic +## Custom stoploss -// TODO: Complete this section +A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. +Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. +The usage of the custom stoploss method must be enabled by setting `custom_stoploss=True` on the strategy object. +The method must return a stoploss value (float / number) with a relative ratio below the current price. +E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`). + +``` python + custom_stoploss = True + + def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, + **kwargs) -> float: + # TODO: Add full docstring here + return 0.04 +``` + +!!! Tip "Trailing stoploss" + It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior. + +### Custom stoploss examples + +Absolute stoploss. The below example sets absolute profit levels based on the current profit. + +* Use the regular stoploss until 20% profit is reached +* Once profit is > 20% - stoploss will be set to 7%.s +* Once profit is > 25% - stoploss will be 15%. +* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. + +``` python + def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, + **kwargs) -> float: + # TODO: Add full docstring here + + # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price + if current_profit > 0.20: + return (-0.7 + current_profit) + if current_profit > 0.25: + return (-0.15 + current_profit) + if current_profit > 0.40: + return (-0.25 + current_profit) + return 1 +``` ## Custom order timeout rules -Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. +Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. -However, freqtrade also offers a custom callback for both ordertypes, which allows you to decide based on custom criteria if a order did time out or not. +However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if a order did time out or not. !!! Note Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. From f235ab8cf4fb148c122b20822f135cb5ce0a8c2a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 11:39:21 +0100 Subject: [PATCH 027/563] Fix some typos in docs --- docs/strategy-advanced.md | 12 ++++++------ docs/strategy-customization.md | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index d4068c82b..02ee9b201 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -73,7 +73,7 @@ The function must return either `True` (cancel order) or `False` (keep order ali from datetime import datetime, timedelta from freqtrade.persistence import Trade -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -112,7 +112,7 @@ class Awesomestrategy(IStrategy): from datetime import datetime from freqtrade.persistence import Trade -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -148,7 +148,7 @@ This can be used to perform calculations which are pair independent (apply to al ``` python import requests -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -173,7 +173,7 @@ class Awesomestrategy(IStrategy): `confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect). ``` python -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -209,7 +209,7 @@ class Awesomestrategy(IStrategy): from freqtrade.persistence import Trade -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -264,4 +264,4 @@ class MyAwesomeStrategy2(MyAwesomeStrategy): trailing_stop = True ``` -Both attributes and methods may be overriden, altering behavior of the original strategy in a way you need. +Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index ab64d3a67..688a1c338 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -309,7 +309,7 @@ Storing information can be accomplished by creating a new dictionary within the The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. ```python -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # Create custom dictionary cust_info = {} From f7b54c24158f4e80ed927d4a333c739f97e09ce3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 11:46:49 +0100 Subject: [PATCH 028/563] Allow and document time-based custom stoploss closes #3206 --- docs/strategy-advanced.md | 28 +++++++++++++++++++++++++--- freqtrade/strategy/interface.py | 5 +++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 02ee9b201..60da11207 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -31,7 +31,9 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l ### Custom stoploss examples -Absolute stoploss. The below example sets absolute profit levels based on the current profit. +#### Absolute stoploss + +The below example sets absolute profit levels based on the current profit. * Use the regular stoploss until 20% profit is reached * Once profit is > 20% - stoploss will be set to 7%.s @@ -39,8 +41,10 @@ Absolute stoploss. The below example sets absolute profit levels based on the cu * Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. ``` python - def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, - **kwargs) -> float: + custom_stoploss = True + + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: # TODO: Add full docstring here # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price @@ -53,6 +57,24 @@ Absolute stoploss. The below example sets absolute profit levels based on the cu return 1 ``` +#### Time based trailing stop + +Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. + +``` python + custom_stoploss = True + + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: + # TODO: Add full docstring here + + if current_time - timedelta(minutes=60) > trade.open_time: + return -0.10 + elif current_time - timedelta(minutes=120) > trade.open_time: + return -0.05 + return 1 +``` + ## Custom order timeout rules Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 7f60ba62f..4574ca9f2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -255,8 +255,8 @@ class IStrategy(ABC): """ return True - def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, - **kwargs) -> float: + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: """ Define custom stoploss logic The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. @@ -554,6 +554,7 @@ class IStrategy(ABC): if self.custom_stoploss: stop_loss_value = strategy_safe_wrapper(self.stoploss_value, default_retval=None )(pair=trade.pair, trade=trade, + current_time=current_time, current_rate=current_rate, current_profit=current_profit) # Sanity check - error cases will return None From b2c109831668936cae9ea1c3eae54ffc763efd2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 11:58:42 +0100 Subject: [PATCH 029/563] more docs for dynamic stoploss method --- docs/strategy-advanced.md | 41 +++++++++++++++++++++++++++++---- freqtrade/strategy/interface.py | 1 + 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 60da11207..f54bec206 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -20,23 +20,29 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l ``` python custom_stoploss = True - def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, - **kwargs) -> float: + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: # TODO: Add full docstring here return 0.04 ``` +!!! Note "Use of dates" + All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. + !!! Tip "Trailing stoploss" It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior. ### Custom stoploss examples +The next section will show some examples on what's possible with the custom stoploss function. +Of course, many more things are possible, and all examples can be combined at will. + #### Absolute stoploss The below example sets absolute profit levels based on the current profit. * Use the regular stoploss until 20% profit is reached -* Once profit is > 20% - stoploss will be set to 7%.s +* Once profit is > 20% - stoploss will be set to 7%. * Once profit is > 25% - stoploss will be 15%. * Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. @@ -68,13 +74,34 @@ Use the initial stoploss for the first 60 minutes, after this change to 10% trai current_profit: float, **kwargs) -> float: # TODO: Add full docstring here - if current_time - timedelta(minutes=60) > trade.open_time: + if current_time - timedelta(minutes=60) > trade.open_date: return -0.10 - elif current_time - timedelta(minutes=120) > trade.open_time: + elif current_time - timedelta(minutes=120) > trade.open_date: return -0.05 return 1 ``` +#### Different stoploss per pair + +Use a different stoploss depending on the pair. +In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs. + +``` python + custom_stoploss = True + + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: + # TODO: Add full docstring here + + if pair in ('ETH/BTC', 'XRP/BTC'): + return -0.10 + elif pair in ('LTC/BTC'): + return -0.05 + return -0.15 +``` + +--- + ## Custom order timeout rules Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. @@ -162,6 +189,8 @@ class AwesomeStrategy(IStrategy): return False ``` +--- + ## Bot loop start callback A simple callback which is called once at the start of every bot throttling iteration. @@ -267,6 +296,8 @@ class AwesomeStrategy(IStrategy): ``` +--- + ## Derived strategies The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4574ca9f2..021674bb9 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -267,6 +267,7 @@ class IStrategy(ABC): :param pair: Pair that's about to be sold. :param trade: trade object. + :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. From 6892c08e9b9091ab68a89ea92ff654316813b7a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 13:18:06 +0100 Subject: [PATCH 030/563] Improve docstring --- freqtrade/strategy/interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 021674bb9..9378f1996 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -258,12 +258,14 @@ class IStrategy(ABC): def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: """ - Define custom stoploss logic + Custom stoploss logic, returning the new distance relative to current_rate (as ratio). + e.g. returning -0.05 would create a stoploss 5% below current_rate. The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, returns the initial stoploss value + Only called when custom_stoploss is set to True. :param pair: Pair that's about to be sold. :param trade: trade object. From 11e29156210828e1d3d97714eb45cfc2fcc237cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 17:40:01 +0100 Subject: [PATCH 031/563] Fix documentation problem --- docs/strategy-advanced.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f54bec206..79a1f0ec5 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -42,9 +42,9 @@ Of course, many more things are possible, and all examples can be combined at wi The below example sets absolute profit levels based on the current profit. * Use the regular stoploss until 20% profit is reached -* Once profit is > 20% - stoploss will be set to 7%. -* Once profit is > 25% - stoploss will be 15%. * Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. +* Once profit is > 25% - stoploss will be 15%. +* Once profit is > 20% - stoploss will be set to 7%. ``` python custom_stoploss = True @@ -54,12 +54,12 @@ The below example sets absolute profit levels based on the current profit. # TODO: Add full docstring here # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price - if current_profit > 0.20: - return (-0.7 + current_profit) - if current_profit > 0.25: - return (-0.15 + current_profit) if current_profit > 0.40: return (-0.25 + current_profit) + if current_profit > 0.25: + return (-0.15 + current_profit) + if current_profit > 0.20: + return (-0.7 + current_profit) return 1 ``` @@ -74,10 +74,11 @@ Use the initial stoploss for the first 60 minutes, after this change to 10% trai current_profit: float, **kwargs) -> float: # TODO: Add full docstring here - if current_time - timedelta(minutes=60) > trade.open_date: - return -0.10 - elif current_time - timedelta(minutes=120) > trade.open_date: + # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. + if current_time - timedelta(minutes=120) > trade.open_date: return -0.05 + elif current_time - timedelta(minutes=60) > trade.open_date: + return -0.10 return 1 ``` From ea4238e86019a275f2f8131cd81d6e0cb51ebac8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 17:59:49 +0100 Subject: [PATCH 032/563] cleanup some tests --- tests/strategy/test_default_strategy.py | 19 ++++++++++++++++++- tests/strategy/test_interface.py | 6 ++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 1b1648db9..c5d76b4c5 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -1,3 +1,5 @@ +from datetime import datetime +from freqtrade.persistence.models import Trade from pandas import DataFrame from .strats.default_strategy import DefaultStrategy @@ -12,7 +14,7 @@ def test_default_strategy_structure(): assert hasattr(DefaultStrategy, 'populate_sell_trend') -def test_default_strategy(result): +def test_default_strategy(result, fee): strategy = DefaultStrategy({}) metadata = {'pair': 'ETH/BTC'} @@ -23,3 +25,18 @@ def test_default_strategy(result): assert type(indicators) is DataFrame assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame + + trade = Trade( + open_rate=19_000, + amount=0.1, + pair='ETH/BTC', + fee_open=fee.return_value + ) + + assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, + rate=20000, time_in_force='gtc') is True + assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, + rate=20000, time_in_force='gtc', sell_reason='roi') is True + + assert strategy.stoploss_value(pair='ETH/BTC', trade=trade, current_time=datetime.now(), + current_rate=20_000, current_profit=0.05) == strategy.stoploss diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 640849ba4..3625c964f 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -105,9 +105,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) -def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): - # default_conf defines a 5m interval. we check interval * 2 + 5m - # this is necessary as the last candle is removed (partial candles) by default +def test_assert_df_raise(mocker, caplog, ohlcv_history): ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) # Take a copy to correctly modify the call mocked_history = ohlcv_history.copy() @@ -127,7 +125,7 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): caplog) -def test_assert_df(default_conf, mocker, ohlcv_history, caplog): +def test_assert_df(ohlcv_history, caplog): df_len = len(ohlcv_history) - 1 # Ensure it's running when passed correctly _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), From 22d64553c91e3f3d6c5c2541ee3363c611a8c6f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 18:00:44 +0100 Subject: [PATCH 033/563] Rename test file --- tests/strategy/{test_strategy.py => test_strategy_loading.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/strategy/{test_strategy.py => test_strategy_loading.py} (100%) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy_loading.py similarity index 100% rename from tests/strategy/test_strategy.py rename to tests/strategy/test_strategy_loading.py From 5f8610b28f9b3367ac371a1a405490d27a16265d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 20:08:03 +0100 Subject: [PATCH 034/563] Add explicit test for stop_loss_reached --- tests/strategy/test_interface.py | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 3625c964f..4aedc0c3a 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 +from freqtrade.strategy.interface import SellCheckTuple, SellType import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -286,6 +287,62 @@ def test_min_roi_reached3(default_conf, fee) -> None: assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) +@pytest.mark.parametrize('profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2', [ + # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, + # enable custom stoploss, expected after 1st call, expected after 2nd call + (0.2, 0.9, SellType.NONE, False, False, 0.3, 0.9, SellType.NONE), + (0.2, 0.9, SellType.NONE, False, False, -0.2, 0.9, SellType.STOP_LOSS), + (0.2, 1.14, SellType.NONE, True, False, 0.05, 1.14, SellType.TRAILING_STOP_LOSS), + (0.01, 0.96, SellType.NONE, True, False, 0.05, 1, SellType.NONE), + (0.05, 1, SellType.NONE, True, False, -0.01, 1, SellType.TRAILING_STOP_LOSS), + # Default custom case - trails with 10% + (0.05, 0.95, SellType.NONE, False, True, -0.02, 0.95, SellType.NONE), + (0.05, 0.95, SellType.NONE, False, True, -0.06, 0.95, SellType.TRAILING_STOP_LOSS), +]) +def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, + profit2, adjusted2, expected2) -> None: + + default_conf.update({'strategy': 'DefaultStrategy'}) + + strategy = StrategyResolver.load_strategy(default_conf) + trade = Trade( + pair='ETH/BTC', + stake_amount=0.01, + amount=1, + open_date=arrow.utcnow().shift(hours=-1).datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='bittrex', + open_rate=1, + ) + trade.adjust_min_max_rates(trade.open_rate) + strategy.trailing_stop = trailing + strategy.trailing_stop_positive = -0.05 + strategy.custom_stoploss = custom + + now = arrow.utcnow().datetime + sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, + current_time=now, current_profit=profit, + force_stoploss=0, high=None) + assert isinstance(sl_flag, SellCheckTuple) + assert sl_flag.sell_type == expected + if expected == SellType.NONE: + assert sl_flag.sell_flag is False + else: + assert sl_flag.sell_flag is True + assert round(trade.stop_loss, 2) == adjusted + + sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade, + current_time=now, current_profit=profit2, + force_stoploss=0, high=None) + assert sl_flag.sell_type == expected2 + if expected2 == SellType.NONE: + assert sl_flag.sell_flag is False + else: + assert sl_flag.sell_flag is True + assert round(trade.stop_loss, 2) == adjusted2 + + def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) From f8639fe93849d9423d1ee8e04c90f9b8738334a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 20:22:32 +0100 Subject: [PATCH 035/563] Add more tests for custom_loss --- tests/strategy/test_interface.py | 44 ++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4aedc0c3a..e8a3ee8de 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -11,7 +11,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.exceptions import StrategyError +from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper @@ -287,20 +287,30 @@ def test_min_roi_reached3(default_conf, fee) -> None: assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) -@pytest.mark.parametrize('profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2', [ - # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, - # enable custom stoploss, expected after 1st call, expected after 2nd call - (0.2, 0.9, SellType.NONE, False, False, 0.3, 0.9, SellType.NONE), - (0.2, 0.9, SellType.NONE, False, False, -0.2, 0.9, SellType.STOP_LOSS), - (0.2, 1.14, SellType.NONE, True, False, 0.05, 1.14, SellType.TRAILING_STOP_LOSS), - (0.01, 0.96, SellType.NONE, True, False, 0.05, 1, SellType.NONE), - (0.05, 1, SellType.NONE, True, False, -0.01, 1, SellType.TRAILING_STOP_LOSS), - # Default custom case - trails with 10% - (0.05, 0.95, SellType.NONE, False, True, -0.02, 0.95, SellType.NONE), - (0.05, 0.95, SellType.NONE, False, True, -0.06, 0.95, SellType.TRAILING_STOP_LOSS), -]) +@pytest.mark.parametrize( + 'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [ + # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, + # enable custom stoploss, expected after 1st call, expected after 2nd call + (0.2, 0.9, SellType.NONE, False, False, 0.3, 0.9, SellType.NONE, None), + (0.2, 0.9, SellType.NONE, False, False, -0.2, 0.9, SellType.STOP_LOSS, None), + (0.2, 1.14, SellType.NONE, True, False, 0.05, 1.14, SellType.TRAILING_STOP_LOSS, None), + (0.01, 0.96, SellType.NONE, True, False, 0.05, 1, SellType.NONE, None), + (0.05, 1, SellType.NONE, True, False, -0.01, 1, SellType.TRAILING_STOP_LOSS, None), + # Default custom case - trails with 10% + (0.05, 0.95, SellType.NONE, False, True, -0.02, 0.95, SellType.NONE, None), + (0.05, 0.95, SellType.NONE, False, True, -0.06, 0.95, SellType.TRAILING_STOP_LOSS, None), + (0.05, 1, SellType.NONE, False, True, -0.06, 1, SellType.TRAILING_STOP_LOSS, + lambda **kwargs: -0.05), + (0.05, 1, SellType.NONE, False, True, 0.09, 1.04, SellType.NONE, + lambda **kwargs: -0.05), + (0.05, 0.95, SellType.NONE, False, True, 0.09, 0.98, SellType.NONE, + lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)), + # Error case - static stoploss in place + (0.05, 0.9, SellType.NONE, False, True, 0.09, 0.9, SellType.NONE, + lambda **kwargs: None), + ]) def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, - profit2, adjusted2, expected2) -> None: + profit2, adjusted2, expected2, custom_stop) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) @@ -319,6 +329,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili strategy.trailing_stop = trailing strategy.trailing_stop_positive = -0.05 strategy.custom_stoploss = custom + original_stopvalue = strategy.stoploss_value + if custom_stop: + strategy.stoploss_value = custom_stop now = arrow.utcnow().datetime sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, @@ -342,6 +355,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili assert sl_flag.sell_flag is True assert round(trade.stop_loss, 2) == adjusted2 + strategy.stoploss_value = original_stopvalue + + def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) From 8574751a07725f5727c30726fa2c7ce996de678a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 10:13:47 +0100 Subject: [PATCH 036/563] Add stoploss_value to strategy template --- docs/strategy-advanced.md | 55 ++++++++++--------- .../subtemplates/strategy_methods_advanced.j2 | 24 ++++++++ 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 79a1f0ec5..e00e3d78d 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -8,6 +8,9 @@ If you're just getting started, please be familiar with the methods described in !!! Note All callback methods described below should only be implemented in a strategy if they are actually used. +!!! Tip + You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` + ## Custom stoploss A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. @@ -37,32 +40,6 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l The next section will show some examples on what's possible with the custom stoploss function. Of course, many more things are possible, and all examples can be combined at will. -#### Absolute stoploss - -The below example sets absolute profit levels based on the current profit. - -* Use the regular stoploss until 20% profit is reached -* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. -* Once profit is > 25% - stoploss will be 15%. -* Once profit is > 20% - stoploss will be set to 7%. - -``` python - custom_stoploss = True - - def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: - # TODO: Add full docstring here - - # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price - if current_profit > 0.40: - return (-0.25 + current_profit) - if current_profit > 0.25: - return (-0.15 + current_profit) - if current_profit > 0.20: - return (-0.7 + current_profit) - return 1 -``` - #### Time based trailing stop Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. @@ -101,6 +78,32 @@ In this example, we'll trail the highest price with 10% trailing stoploss for `E return -0.15 ``` +#### Absolute stoploss + +The below example sets absolute profit levels based on the current profit. + +* Use the regular stoploss until 20% profit is reached +* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. +* Once profit is > 25% - stoploss will be 15%. +* Once profit is > 20% - stoploss will be set to 7%. + +``` python + custom_stoploss = True + + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: + # TODO: Add full docstring here + + # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price + if current_profit > 0.40: + return (-0.25 + current_profit) + if current_profit > 0.25: + return (-0.15 + current_profit) + if current_profit > 0.20: + return (-0.7 + current_profit) + return 1 +``` + --- ## Custom order timeout rules diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 5ca6e6971..e6ae477b9 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -12,6 +12,30 @@ def bot_loop_start(self, **kwargs) -> None: """ pass +custom_stoploss = True + +def stoploss_value(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **kwargs) -> float: + """ + Custom stoploss logic, returning the new distance relative to current_rate (as ratio). + e.g. returning -0.05 would create a stoploss 5% below current_rate. + The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns the initial stoploss value + Only called when custom_stoploss is set to True. + + :param pair: Pair that's about to be sold. + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: New stoploss value, relative to the currentrate + """ + return self.stoploss + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, **kwargs) -> bool: """ From 277342f1679f1dea80fa24ed7061424afebf97a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 11:12:22 +0100 Subject: [PATCH 037/563] Rename flag to "use_custom_stoposs" --- docs/strategy-advanced.md | 10 +++++----- freqtrade/strategy/interface.py | 6 +++--- .../subtemplates/strategy_methods_advanced.j2 | 4 ++-- tests/strategy/test_interface.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index e00e3d78d..f2d8e4151 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -16,12 +16,12 @@ If you're just getting started, please be familiar with the methods described in A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. -The usage of the custom stoploss method must be enabled by setting `custom_stoploss=True` on the strategy object. +The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object. The method must return a stoploss value (float / number) with a relative ratio below the current price. E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`). ``` python - custom_stoploss = True + use_custom_stoploss = True def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: @@ -45,7 +45,7 @@ Of course, many more things are possible, and all examples can be combined at wi Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. ``` python - custom_stoploss = True + use_custom_stoploss = True def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: @@ -65,7 +65,7 @@ Use a different stoploss depending on the pair. In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs. ``` python - custom_stoploss = True + use_custom_stoploss = True def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: @@ -88,7 +88,7 @@ The below example sets absolute profit levels based on the current profit. * Once profit is > 20% - stoploss will be set to 7%. ``` python - custom_stoploss = True + use_custom_stoploss = True def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9378f1996..d93dda849 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -89,7 +89,7 @@ class IStrategy(ABC): trailing_stop_positive: Optional[float] = None trailing_stop_positive_offset: float = 0.0 trailing_only_offset_is_reached = False - custom_stoploss: bool = False + use_custom_stoploss: bool = False # associated timeframe ticker_interval: str # DEPRECATED @@ -265,7 +265,7 @@ class IStrategy(ABC): For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, returns the initial stoploss value - Only called when custom_stoploss is set to True. + Only called when use_custom_stoploss is set to True. :param pair: Pair that's about to be sold. :param trade: trade object. @@ -554,7 +554,7 @@ class IStrategy(ABC): # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) - if self.custom_stoploss: + if self.use_custom_stoploss: stop_loss_value = strategy_safe_wrapper(self.stoploss_value, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index e6ae477b9..0ae3e077c 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -12,7 +12,7 @@ def bot_loop_start(self, **kwargs) -> None: """ pass -custom_stoploss = True +use_custom_stoploss = True def stoploss_value(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, current_profit: float, **kwargs) -> float: @@ -24,7 +24,7 @@ def stoploss_value(self, pair: str, trade: 'Trade', current_time: 'datetime', cu For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, returns the initial stoploss value - Only called when custom_stoploss is set to True. + Only called when use_custom_stoploss is set to True. :param pair: Pair that's about to be sold. :param trade: trade object. diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e8a3ee8de..5922ad9b2 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -328,7 +328,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili trade.adjust_min_max_rates(trade.open_rate) strategy.trailing_stop = trailing strategy.trailing_stop_positive = -0.05 - strategy.custom_stoploss = custom + strategy.use_custom_stoploss = custom original_stopvalue = strategy.stoploss_value if custom_stop: strategy.stoploss_value = custom_stop From 9d5961e2247295d086023cacc9d959722404d0f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 11:17:50 +0100 Subject: [PATCH 038/563] Rename method to custom_stoploss --- docs/strategy-advanced.md | 16 ++++++++-------- freqtrade/strategy/interface.py | 6 +++--- .../subtemplates/strategy_methods_advanced.j2 | 4 ++-- tests/strategy/test_default_strategy.py | 8 +++++--- tests/strategy/test_interface.py | 11 +++++------ 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f2d8e4151..bb455ff9f 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -23,8 +23,8 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l ``` python use_custom_stoploss = True - def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: # TODO: Add full docstring here return 0.04 ``` @@ -47,8 +47,8 @@ Use the initial stoploss for the first 60 minutes, after this change to 10% trai ``` python use_custom_stoploss = True - def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: # TODO: Add full docstring here # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. @@ -67,8 +67,8 @@ In this example, we'll trail the highest price with 10% trailing stoploss for `E ``` python use_custom_stoploss = True - def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: # TODO: Add full docstring here if pair in ('ETH/BTC', 'XRP/BTC'): @@ -90,8 +90,8 @@ The below example sets absolute profit levels based on the current profit. ``` python use_custom_stoploss = True - def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: # TODO: Add full docstring here # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d93dda849..61c879e8f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -255,8 +255,8 @@ class IStrategy(ABC): """ return True - def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -555,7 +555,7 @@ class IStrategy(ABC): trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) if self.use_custom_stoploss: - stop_loss_value = strategy_safe_wrapper(self.stoploss_value, default_retval=None + stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 0ae3e077c..f4cda2e89 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -14,8 +14,8 @@ def bot_loop_start(self, **kwargs) -> None: use_custom_stoploss = True -def stoploss_value(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, **kwargs) -> float: +def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index c5d76b4c5..ec7b3c33d 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -1,7 +1,9 @@ from datetime import datetime -from freqtrade.persistence.models import Trade + from pandas import DataFrame +from freqtrade.persistence.models import Trade + from .strats.default_strategy import DefaultStrategy @@ -38,5 +40,5 @@ def test_default_strategy(result, fee): assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', sell_reason='roi') is True - assert strategy.stoploss_value(pair='ETH/BTC', trade=trade, current_time=datetime.now(), - current_rate=20_000, current_profit=0.05) == strategy.stoploss + assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), + current_rate=20_000, current_profit=0.05) == strategy.stoploss diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5922ad9b2..7eed43302 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 -from freqtrade.strategy.interface import SellCheckTuple, SellType import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -11,9 +10,10 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.exceptions import OperationalException, StrategyError +from freqtrade.exceptions import StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver +from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -329,9 +329,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili strategy.trailing_stop = trailing strategy.trailing_stop_positive = -0.05 strategy.use_custom_stoploss = custom - original_stopvalue = strategy.stoploss_value + original_stopvalue = strategy.custom_stoploss if custom_stop: - strategy.stoploss_value = custom_stop + strategy.custom_stoploss = custom_stop now = arrow.utcnow().datetime sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, @@ -355,8 +355,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili assert sl_flag.sell_flag is True assert round(trade.stop_loss, 2) == adjusted2 - strategy.stoploss_value = original_stopvalue - + strategy.custom_stoploss = original_stopvalue def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: From 676dd0d664e9ee59f2008b518eb12795e3fe9d85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 11:22:15 +0100 Subject: [PATCH 039/563] Improve documentation --- docs/stoploss.md | 1 + docs/strategy-advanced.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/stoploss.md b/docs/stoploss.md index 1e21fc50d..671e643b0 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -78,6 +78,7 @@ At this stage the bot contains the following stoploss support modes: 2. Trailing stop loss. 3. Trailing stop loss, custom positive loss. 4. Trailing stop loss only once the trade has reached a certain offset. +5. [Custom stoploss function](strategy-advanced.md#custom-stoploss) ### Static Stop Loss diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index bb455ff9f..4833fbade 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -29,6 +29,8 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l return 0.04 ``` +Stoploss on exchange works similar to trailing_stop, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). + !!! Note "Use of dates" All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. From fc0d14c1b5c9b15ef04d5b9abb32283bc38bd78c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 19:14:18 +0100 Subject: [PATCH 040/563] Improve documentation --- docs/strategy-advanced.md | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4833fbade..49720d729 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -20,16 +20,35 @@ The usage of the custom stoploss method must be enabled by setting `use_custom_s The method must return a stoploss value (float / number) with a relative ratio below the current price. E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`). +To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: + ``` python use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # TODO: Add full docstring here - return 0.04 + """ + Custom stoploss logic, returning the new distance relative to current_rate (as ratio). + e.g. returning -0.05 would create a stoploss 5% below current_rate. + The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns the initial stoploss value + Only called when use_custom_stoploss is set to True. + + :param pair: Pair that's about to be sold. + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: New stoploss value, relative to the currentrate + """ + return -0.04 ``` -Stoploss on exchange works similar to trailing_stop, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). +Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). !!! Note "Use of dates" All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. @@ -51,7 +70,6 @@ Use the initial stoploss for the first 60 minutes, after this change to 10% trai def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # TODO: Add full docstring here # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. if current_time - timedelta(minutes=120) > trade.open_date: @@ -71,7 +89,6 @@ In this example, we'll trail the highest price with 10% trailing stoploss for `E def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # TODO: Add full docstring here if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -94,7 +111,6 @@ The below example sets absolute profit levels based on the current profit. def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # TODO: Add full docstring here # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price if current_profit > 0.40: From 36d60fa8a8b7ee6fc2118808aa14d7ef19079111 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 19:54:27 +0200 Subject: [PATCH 041/563] First small compat test --- tests/exchange/test_ccxt_compat.py | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/exchange/test_ccxt_compat.py diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py new file mode 100644 index 000000000..e6deebf62 --- /dev/null +++ b/tests/exchange/test_ccxt_compat.py @@ -0,0 +1,43 @@ +""" +Tests in this file do NOT mock network calls, so they are expected to be fluky at times. + +However, these tests should give a good idea to determine if a new exchange is +suitable to run with freqtrade. + +""" + +from freqtrade.resolvers.exchange_resolver import ExchangeResolver +import pytest + +# Exchanges that should be tested +EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] + + +@pytest.fixture +def exchange_conf(default_conf): + default_conf['exchange']['pair_whitelist'] = [] + return default_conf + + +@pytest.mark.parametrize('exchange', EXCHANGES) +def test_ccxt_fetch_l2_orderbook(exchange_conf, exchange): + + exchange_conf['exchange']['name'] = exchange + exchange_conf['exchange']['name'] = exchange + + exchange = ExchangeResolver.load_exchange(exchange, exchange_conf) + l2 = exchange.fetch_l2_order_book('BTC/USDT') + assert 'asks' in l2 + assert 'bids' in l2 + + for val in [1, 2, 5, 25, 100]: + l2 = exchange.fetch_l2_order_book('BTC/USDT', val) + if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: + assert len(l2['asks']) == val + assert len(l2['bids']) == val + else: + next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) + assert len(l2['asks']) == next_limit + assert len(l2['asks']) == next_limit + + From 38af1b2a5dd51ce659b1e7ee13f30237ff332afa Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Oct 2020 20:46:01 +0200 Subject: [PATCH 042/563] Improve compat tests --- tests/conftest.py | 6 +- tests/exchange/test_ccxt_compat.py | 109 ++++++++++++++++++++++------- 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 965980f7a..e7e9a3096 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -223,7 +223,11 @@ def init_persistence(default_conf): @pytest.fixture(scope="function") -def default_conf(testdatadir): +def default_conf(): + return get_default_conf() + + +def get_default_conf(testdatadir): """ Returns validated configuration suitable for most tests """ configuration = { "max_open_trades": 1, diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index e6deebf62..6b6fbcfc9 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -6,38 +6,95 @@ suitable to run with freqtrade. """ -from freqtrade.resolvers.exchange_resolver import ExchangeResolver import pytest +from pathlib import Path +from freqtrade.resolvers.exchange_resolver import ExchangeResolver +from tests.conftest import get_default_conf + # Exchanges that should be tested -EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] +EXCHANGES = { + 'bittrex': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': False + }, + 'binance': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True + }, + 'kraken': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True + }, + 'ftx': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True + } +} -@pytest.fixture -def exchange_conf(default_conf): - default_conf['exchange']['pair_whitelist'] = [] - return default_conf +@pytest.fixture(scope="class") +def exchange_conf(): + config = get_default_conf((Path(__file__).parent / "testdata").resolve()) + config['exchange']['pair_whitelist'] = [] + return config -@pytest.mark.parametrize('exchange', EXCHANGES) -def test_ccxt_fetch_l2_orderbook(exchange_conf, exchange): - - exchange_conf['exchange']['name'] = exchange - exchange_conf['exchange']['name'] = exchange - - exchange = ExchangeResolver.load_exchange(exchange, exchange_conf) - l2 = exchange.fetch_l2_order_book('BTC/USDT') - assert 'asks' in l2 - assert 'bids' in l2 - - for val in [1, 2, 5, 25, 100]: - l2 = exchange.fetch_l2_order_book('BTC/USDT', val) - if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: - assert len(l2['asks']) == val - assert len(l2['bids']) == val - else: - next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) - assert len(l2['asks']) == next_limit - assert len(l2['asks']) == next_limit +@pytest.fixture(params=EXCHANGES, scope="class") +def exchange(request, exchange_conf): + exchange_conf['exchange']['name'] = request.param + exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=False) + yield exchange, request.param +class TestCCXTExchange(): + + def test_load_markets(self, exchange): + exchange, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + markets = exchange.markets + assert pair in markets + assert isinstance(markets[pair], dict) + + def test_ccxt_fetch_tickers(self, exchange): + exchange, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + + tickers = exchange.get_tickers() + assert pair in tickers + assert 'ask' in tickers[pair] + assert tickers[pair]['ask'] is not None + assert 'bid' in tickers[pair] + assert tickers[pair]['bid'] is not None + assert 'quoteVolume' in tickers[pair] + if EXCHANGES[exchangename].get('hasQuoteVolume'): + assert tickers[pair]['quoteVolume'] is not None + + def test_ccxt_fetch_ticker(self, exchange): + exchange, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + + ticker = exchange.fetch_ticker(pair) + assert 'ask' in ticker + assert ticker['ask'] is not None + assert 'bid' in ticker + assert ticker['bid'] is not None + assert 'quoteVolume' in ticker + if EXCHANGES[exchangename].get('hasQuoteVolume'): + assert ticker['quoteVolume'] is not None + + def test_ccxt_fetch_l2_orderbook(self, exchange): + exchange, exchangename = exchange + l2 = exchange.fetch_l2_order_book('BTC/USDT') + assert 'asks' in l2 + assert 'bids' in l2 + + for val in [1, 2, 5, 25, 100]: + l2 = exchange.fetch_l2_order_book('BTC/USDT', val) + if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: + assert len(l2['asks']) == val + assert len(l2['bids']) == val + else: + next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) + assert len(l2['asks']) == next_limit + assert len(l2['asks']) == next_limit From 79ed89e4872705f40780266dc08811b1a019e6cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Oct 2020 20:49:46 +0200 Subject: [PATCH 043/563] Add test for fee calculation --- tests/exchange/test_ccxt_compat.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 6b6fbcfc9..082d1dcf4 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -98,3 +98,12 @@ class TestCCXTExchange(): next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit + + def test_ccxt_get_fee(self, exchange): + exchange, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + + assert exchange.get_fee(pair, 'limit', 'buy') > 0 < 1 + assert exchange.get_fee(pair, 'limit', 'sell') > 0 < 1 + assert exchange.get_fee(pair, 'market', 'buy') > 0 < 1 + assert exchange.get_fee(pair, 'market', 'sell') > 0 < 1 From 7833d9935cd73cf1e78887308dff64e73fe51605 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Oct 2020 20:50:31 +0200 Subject: [PATCH 044/563] Add dummy test for fetch_ohlcv --- tests/exchange/test_ccxt_compat.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 082d1dcf4..c1b8ab6f3 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -99,6 +99,10 @@ class TestCCXTExchange(): assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit + def test_fetch_ohlcv(self, exchange): + # TODO: Implement me + pass + def test_ccxt_get_fee(self, exchange): exchange, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] From b7d4ff9c216c7e37d906e5dc06e20daf3c463b35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 13:14:45 +0200 Subject: [PATCH 045/563] Add test for fetch_ohlcv (against exchange) --- tests/exchange/test_ccxt_compat.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index c1b8ab6f3..fa3bd45c8 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -3,7 +3,6 @@ Tests in this file do NOT mock network calls, so they are expected to be fluky a However, these tests should give a good idea to determine if a new exchange is suitable to run with freqtrade. - """ import pytest @@ -16,19 +15,23 @@ from tests.conftest import get_default_conf EXCHANGES = { 'bittrex': { 'pair': 'BTC/USDT', - 'hasQuoteVolume': False + 'hasQuoteVolume': False, + 'timeframe': '5m', }, 'binance': { 'pair': 'BTC/USDT', - 'hasQuoteVolume': True + 'hasQuoteVolume': True, + 'timeframe': '5m', }, 'kraken': { 'pair': 'BTC/USDT', - 'hasQuoteVolume': True + 'hasQuoteVolume': True, + 'timeframe': '5m', }, 'ftx': { 'pair': 'BTC/USDT', - 'hasQuoteVolume': True + 'hasQuoteVolume': True, + 'timeframe': '5m', } } @@ -100,8 +103,13 @@ class TestCCXTExchange(): assert len(l2['asks']) == next_limit def test_fetch_ohlcv(self, exchange): - # TODO: Implement me - pass + exchange, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + timeframe = EXCHANGES[exchangename]['timeframe'] + pair_tf = (pair, timeframe) + ohlcv = exchange.refresh_latest_ohlcv([pair_tf]) + assert isinstance(ohlcv, list) + assert len(exchange.klines(pair_tf)) > 200 def test_ccxt_get_fee(self, exchange): exchange, exchangename = exchange From a6e6ce16b116cd299090f133ce5e50c6be06b713 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 10:31:24 +0100 Subject: [PATCH 046/563] Fix test failures --- tests/conftest.py | 4 ++-- tests/exchange/test_ccxt_compat.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e7e9a3096..a57b8c505 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -223,8 +223,8 @@ def init_persistence(default_conf): @pytest.fixture(scope="function") -def default_conf(): - return get_default_conf() +def default_conf(testdatadir): + return get_default_conf(testdatadir) def get_default_conf(testdatadir): diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index fa3bd45c8..1e8ddf319 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -88,12 +88,13 @@ class TestCCXTExchange(): def test_ccxt_fetch_l2_orderbook(self, exchange): exchange, exchangename = exchange - l2 = exchange.fetch_l2_order_book('BTC/USDT') + pair = EXCHANGES[exchangename]['pair'] + l2 = exchange.fetch_l2_order_book(pair) assert 'asks' in l2 assert 'bids' in l2 for val in [1, 2, 5, 25, 100]: - l2 = exchange.fetch_l2_order_book('BTC/USDT', val) + l2 = exchange.fetch_l2_order_book(pair, val) if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: assert len(l2['asks']) == val assert len(l2['bids']) == val From 2016eea2120dab76968f49237e967689a515e3c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:29:39 +0100 Subject: [PATCH 047/563] Fix some test-errors in ccxt_compat --- tests/exchange/test_ccxt_compat.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 1e8ddf319..25b26d489 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -100,8 +100,12 @@ class TestCCXTExchange(): assert len(l2['bids']) == val else: next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) - assert len(l2['asks']) == next_limit - assert len(l2['asks']) == next_limit + if next_limit > 200: + assert len(l2['asks']) > 200 + assert len(l2['asks']) > 200 + else: + assert len(l2['asks']) == next_limit + assert len(l2['asks']) == next_limit def test_fetch_ohlcv(self, exchange): exchange, exchangename = exchange @@ -116,7 +120,7 @@ class TestCCXTExchange(): exchange, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] - assert exchange.get_fee(pair, 'limit', 'buy') > 0 < 1 - assert exchange.get_fee(pair, 'limit', 'sell') > 0 < 1 - assert exchange.get_fee(pair, 'market', 'buy') > 0 < 1 - assert exchange.get_fee(pair, 'market', 'sell') > 0 < 1 + assert 0 < exchange.get_fee(pair, 'limit', 'buy') < 1 + assert 0 < exchange.get_fee(pair, 'limit', 'sell') < 1 + assert 0 < exchange.get_fee(pair, 'market', 'buy') < 1 + assert 0 < exchange.get_fee(pair, 'market', 'sell') < 1 From 0981287c62398724ffc16ee80fc7165b3c37c8bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:41:23 +0100 Subject: [PATCH 048/563] Improve test syntax for ccxt_compat tests --- tests/exchange/test_ccxt_compat.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 25b26d489..09cc50f14 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -92,15 +92,16 @@ class TestCCXTExchange(): l2 = exchange.fetch_l2_order_book(pair) assert 'asks' in l2 assert 'bids' in l2 - + l2_limit_range = exchange._ft_has['l2_limit_range'] for val in [1, 2, 5, 25, 100]: l2 = exchange.fetch_l2_order_book(pair, val) - if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: + if not l2_limit_range or val in l2_limit_range: assert len(l2['asks']) == val assert len(l2['bids']) == val else: - next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) + next_limit = exchange.get_next_limit_in_list(val, l2_limit_range) if next_limit > 200: + # Large orderbook sizes can be a problem for some exchanges (bitrex ...) assert len(l2['asks']) > 200 assert len(l2['asks']) > 200 else: @@ -116,6 +117,8 @@ class TestCCXTExchange(): assert isinstance(ohlcv, list) assert len(exchange.klines(pair_tf)) > 200 + # TODO: tests fetch_trades (?) + def test_ccxt_get_fee(self, exchange): exchange, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] From b39de171c88af56edc0b9a773a1d507583264c7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:46:08 +0100 Subject: [PATCH 049/563] Don't run longrun regularily --- .github/workflows/ci.yml | 6 ++++++ tests/conftest.py | 13 +++++++++++++ tests/exchange/test_ccxt_compat.py | 1 + 3 files changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36a9fc374..daa10fea7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,12 @@ jobs: - name: Tests run: | pytest --random-order --cov=freqtrade --cov-config=.coveragerc + if: matrix.python-version != '3.9' + + - name: Tests incl. ccxt compatibility tests + run: | + pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun + if: matrix.python-version == '3.9' - name: Coveralls if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8') diff --git a/tests/conftest.py b/tests/conftest.py index a57b8c505..9eda0e973 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,6 +33,19 @@ logging.getLogger('').setLevel(logging.INFO) np.seterr(all='raise') +def pytest_addoption(parser): + parser.addoption('--longrun', action='store_true', dest="longrun", + default=False, help="Enable long-run tests (ccxt compat)") + + +def pytest_configure(config): + config.addinivalue_line( + "markers", "longrun: mark test that is running slowly and should not be run regularily" + ) + if not config.option.longrun: + setattr(config.option, 'markexpr', 'not longrun') + + def log_has(line, logs): # caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar') # and we want to match line against foobar in the tuple diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 09cc50f14..90cdebcdd 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -50,6 +50,7 @@ def exchange(request, exchange_conf): yield exchange, request.param +@pytest.mark.longrun class TestCCXTExchange(): def test_load_markets(self, exchange): From 5599490aa286675d57dfe3860330faf5af23c402 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:50:24 +0100 Subject: [PATCH 050/563] Adjust ohlcv test after rebase --- tests/exchange/test_ccxt_compat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 90cdebcdd..6d495582b 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -115,7 +115,8 @@ class TestCCXTExchange(): timeframe = EXCHANGES[exchangename]['timeframe'] pair_tf = (pair, timeframe) ohlcv = exchange.refresh_latest_ohlcv([pair_tf]) - assert isinstance(ohlcv, list) + assert isinstance(ohlcv, dict) + assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) assert len(exchange.klines(pair_tf)) > 200 # TODO: tests fetch_trades (?) From 65d91b7cbb6ab4a388d66dc2834edc94fecb1cf2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:53:41 +0100 Subject: [PATCH 051/563] Add note on adding new exchange with compat tests --- docs/developer.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index dcbaa3ca9..6440dba82 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -242,6 +242,8 @@ The `IProtection` parent class provides a helper method for this in `calculate_l Most exchanges supported by CCXT should work out of the box. +To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun`. + ### Stoploss On Exchange Check if the new exchange supports Stoploss on Exchange orders through their API. From 721d0fb2a8c4b5c3f582436c05cfa1ff1086475a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:55:46 +0100 Subject: [PATCH 052/563] Improve wording of developer docs --- docs/developer.md | 3 ++- tests/exchange/test_ccxt_compat.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 6440dba82..de489a348 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -242,7 +242,8 @@ The `IProtection` parent class provides a helper method for this in `calculate_l Most exchanges supported by CCXT should work out of the box. -To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun`. +To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`. +Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar). ### Stoploss On Exchange diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 6d495582b..8db56685a 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -5,8 +5,10 @@ However, these tests should give a good idea to determine if a new exchange is suitable to run with freqtrade. """ -import pytest from pathlib import Path + +import pytest + from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_default_conf From 1713841d0b0ed672c9f16229a482497d1b296df3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 16:20:17 +0100 Subject: [PATCH 053/563] Initialize markets at startup for ccxt tests --- tests/exchange/test_ccxt_compat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 8db56685a..0c8b7bdcf 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -48,7 +48,8 @@ def exchange_conf(): @pytest.fixture(params=EXCHANGES, scope="class") def exchange(request, exchange_conf): exchange_conf['exchange']['name'] = request.param - exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=False) + exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True) + yield exchange, request.param From 1508e08ea5f60b6dbcd5ad4faf085170f09cecd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 08:36:35 +0100 Subject: [PATCH 054/563] Move fiatconvert init to RPC class --- freqtrade/rpc/api_server.py | 4 ---- freqtrade/rpc/rpc.py | 4 +++- freqtrade/rpc/telegram.py | 3 --- tests/rpc/test_rpc.py | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 31e7f3ff2..804c83207 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -20,7 +20,6 @@ from freqtrade.__init__ import __version__ from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException from freqtrade.persistence import Trade -from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.rpc import RPC, RPCException @@ -117,9 +116,6 @@ class ApiServer(RPC): # Register application handling self.register_rest_rpc_urls() - if self._config.get('fiat_display_currency', None): - self._fiat_converter = CryptoToFiatConverter() - thread = threading.Thread(target=self.run, daemon=True) thread.start() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9b7d62b54..8c8e42c28 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -69,7 +69,7 @@ class RPC: """ RPC class can be used to have extra feature, like bot data, and access to DB data """ - # Bind _fiat_converter if needed in each RPC handler + # Bind _fiat_converter if needed _fiat_converter: Optional[CryptoToFiatConverter] = None def __init__(self, freqtrade) -> None: @@ -80,6 +80,8 @@ class RPC: """ self._freqtrade = freqtrade self._config: Dict[str, Any] = freqtrade.config + if self._config.get('fiat_display_currency', None): + self._fiat_converter = CryptoToFiatConverter() @property def name(self) -> str: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index dddba7457..e15071845 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -19,7 +19,6 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC, RPCException, RPCMessageType -from freqtrade.rpc.fiat_convert import CryptoToFiatConverter logger = logging.getLogger(__name__) @@ -77,8 +76,6 @@ class Telegram(RPC): self._updater: Updater self._init_keyboard() self._init() - if self._config.get('fiat_display_currency', None): - self._fiat_converter = CryptoToFiatConverter() def _init_keyboard(self) -> None: """ diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4b36f4b4e..19788c067 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -185,7 +185,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: fetch_ticker=ticker, get_fee=fee, ) - + del default_conf['fiat_display_currency'] freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) From a87c273903743d74e9c8ba5de866159669d2dd80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 09:01:53 +0100 Subject: [PATCH 055/563] Refactor RPC modules so handlers don't inherit RPC directly --- freqtrade/rpc/__init__.py | 2 +- freqtrade/rpc/api_server.py | 69 +++++++++++++++++---------------- freqtrade/rpc/rpc.py | 39 ++++++++++++------- freqtrade/rpc/rpc_manager.py | 19 ++++----- freqtrade/rpc/telegram.py | 69 +++++++++++++++++---------------- freqtrade/rpc/webhook.py | 13 ++++--- tests/rpc/test_rpc_apiserver.py | 11 +++--- tests/rpc/test_rpc_telegram.py | 54 ++++++++++++++++---------- tests/rpc/test_rpc_webhook.py | 12 +++--- 9 files changed, 160 insertions(+), 128 deletions(-) diff --git a/freqtrade/rpc/__init__.py b/freqtrade/rpc/__init__.py index 88978519b..0a0130ca7 100644 --- a/freqtrade/rpc/__init__.py +++ b/freqtrade/rpc/__init__.py @@ -1,3 +1,3 @@ # flake8: noqa: F401 -from .rpc import RPC, RPCException, RPCMessageType +from .rpc import RPC, RPCException, RPCHandler, RPCMessageType from .rpc_manager import RPCManager diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 804c83207..b489586c8 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -20,7 +20,7 @@ from freqtrade.__init__ import __version__ from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException from freqtrade.persistence import Trade -from freqtrade.rpc.rpc import RPC, RPCException +from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler logger = logging.getLogger(__name__) @@ -78,7 +78,7 @@ def shutdown_session(exception=None): Trade.session.remove() -class ApiServer(RPC): +class ApiServer(RPCHandler): """ This class runs api server and provides rpc.rpc functionality to it @@ -89,13 +89,14 @@ class ApiServer(RPC): return (safe_str_cmp(username, self._config['api_server'].get('username')) and safe_str_cmp(password, self._config['api_server'].get('password'))) - def __init__(self, freqtrade) -> None: + def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: """ - Init the api server, and init the super class RPC - :param freqtrade: Instance of a freqtrade bot + Init the api server, and init the super class RPCHandler + :param rpc: instance of RPC Helper class + :param config: Configuration object :return: None """ - super().__init__(freqtrade) + super().__init__(rpc, config) self.app = Flask(__name__) self._cors = CORS(self.app, @@ -282,7 +283,7 @@ class ApiServer(RPC): Handler for /start. Starts TradeThread in bot if stopped. """ - msg = self._rpc_start() + msg = self._rpc._rpc_start() return jsonify(msg) @require_login @@ -292,7 +293,7 @@ class ApiServer(RPC): Handler for /stop. Stops TradeThread in bot if running """ - msg = self._rpc_stop() + msg = self._rpc._rpc_stop() return jsonify(msg) @require_login @@ -302,7 +303,7 @@ class ApiServer(RPC): Handler for /stopbuy. Sets max_open_trades to 0 and gracefully sells all open trades """ - msg = self._rpc_stopbuy() + msg = self._rpc._rpc_stopbuy() return jsonify(msg) @rpc_catch_errors @@ -326,7 +327,7 @@ class ApiServer(RPC): """ Prints the bot's version """ - return jsonify(RPC._rpc_show_config(self._config, self._freqtrade.state)) + return jsonify(RPC._rpc_show_config(self._config, self._rpc._freqtrade.state)) @require_login @rpc_catch_errors @@ -335,7 +336,7 @@ class ApiServer(RPC): Handler for /reload_config. Triggers a config file reload """ - msg = self._rpc_reload_config() + msg = self._rpc._rpc_reload_config() return jsonify(msg) @require_login @@ -345,7 +346,7 @@ class ApiServer(RPC): Handler for /count. Returns the number of trades running """ - msg = self._rpc_count() + msg = self._rpc._rpc_count() return jsonify(msg) @require_login @@ -355,7 +356,7 @@ class ApiServer(RPC): Handler for /locks. Returns the currently active locks. """ - return jsonify(self._rpc_locks()) + return jsonify(self._rpc._rpc_locks()) @require_login @rpc_catch_errors @@ -368,10 +369,10 @@ class ApiServer(RPC): timescale = request.args.get('timescale', 7) timescale = int(timescale) - stats = self._rpc_daily_profit(timescale, - self._config['stake_currency'], - self._config.get('fiat_display_currency', '') - ) + stats = self._rpc._rpc_daily_profit(timescale, + self._config['stake_currency'], + self._config.get('fiat_display_currency', '') + ) return jsonify(stats) @@ -394,7 +395,7 @@ class ApiServer(RPC): Returns information related to Edge. :return: edge stats """ - stats = self._rpc_edge() + stats = self._rpc._rpc_edge() return jsonify(stats) @@ -408,9 +409,9 @@ class ApiServer(RPC): :return: stats """ - stats = self._rpc_trade_statistics(self._config['stake_currency'], - self._config.get('fiat_display_currency') - ) + stats = self._rpc._rpc_trade_statistics(self._config['stake_currency'], + self._config.get('fiat_display_currency') + ) return jsonify(stats) @@ -422,7 +423,7 @@ class ApiServer(RPC): Returns a Object with "durations" and "sell_reasons" as keys. """ - stats = self._rpc_stats() + stats = self._rpc._rpc_stats() return jsonify(stats) @@ -435,7 +436,7 @@ class ApiServer(RPC): Returns a cumulative performance statistics :return: stats """ - stats = self._rpc_performance() + stats = self._rpc._rpc_performance() return jsonify(stats) @@ -448,7 +449,7 @@ class ApiServer(RPC): Returns the current status of the trades in json format """ try: - results = self._rpc_trade_status() + results = self._rpc._rpc_trade_status() return jsonify(results) except RPCException: return jsonify([]) @@ -461,8 +462,8 @@ class ApiServer(RPC): Returns the current status of the trades in json format """ - results = self._rpc_balance(self._config['stake_currency'], - self._config.get('fiat_display_currency', '')) + results = self._rpc._rpc_balance(self._config['stake_currency'], + self._config.get('fiat_display_currency', '')) return jsonify(results) @require_login @@ -474,7 +475,7 @@ class ApiServer(RPC): Returns the X last trades in json format """ limit = int(request.args.get('limit', 0)) - results = self._rpc_trade_history(limit) + results = self._rpc._rpc_trade_history(limit) return jsonify(results) @require_login @@ -487,7 +488,7 @@ class ApiServer(RPC): param: tradeid: Numeric trade-id assigned to the trade. """ - result = self._rpc_delete(tradeid) + result = self._rpc._rpc_delete(tradeid) return jsonify(result) @require_login @@ -496,7 +497,7 @@ class ApiServer(RPC): """ Handler for /whitelist. """ - results = self._rpc_whitelist() + results = self._rpc._rpc_whitelist() return jsonify(results) @require_login @@ -506,7 +507,7 @@ class ApiServer(RPC): Handler for /blacklist. """ add = request.json.get("blacklist", None) if request.method == 'POST' else None - results = self._rpc_blacklist(add) + results = self._rpc._rpc_blacklist(add) return jsonify(results) @require_login @@ -519,7 +520,7 @@ class ApiServer(RPC): price = request.json.get("price", None) price = float(price) if price is not None else price - trade = self._rpc_forcebuy(asset, price) + trade = self._rpc._rpc_forcebuy(asset, price) if trade: return jsonify(trade.to_json()) else: @@ -532,7 +533,7 @@ class ApiServer(RPC): Handler for /forcesell. """ tradeid = request.json.get("tradeid") - results = self._rpc_forcesell(tradeid) + results = self._rpc._rpc_forcesell(tradeid) return jsonify(results) @require_login @@ -554,7 +555,7 @@ class ApiServer(RPC): if not pair or not timeframe: return self.rest_error("Mandatory parameter missing.", 400) - results = self._rpc_analysed_dataframe(pair, timeframe, limit) + results = self._rpc._rpc_analysed_dataframe(pair, timeframe, limit) return jsonify(results) @require_login @@ -593,7 +594,7 @@ class ApiServer(RPC): """ Handler for /plot_config. """ - return jsonify(self._rpc_plot_config()) + return jsonify(self._rpc._rpc_plot_config()) @require_login @rpc_catch_errors diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8c8e42c28..42ab76622 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -65,6 +65,32 @@ class RPCException(Exception): } +class RPCHandler: + + def __init__(self, rpc: 'RPC', config: Dict[str, Any]) -> None: + """ + Initializes RPCHandlers + :param rpc: instance of RPC Helper class + :param config: Configuration object + :return: None + """ + self._rpc = rpc + self._config: Dict[str, Any] = config + + @property + def name(self) -> str: + """ Returns the lowercase name of the implementation """ + return self.__class__.__name__.lower() + + @abstractmethod + def cleanup(self) -> None: + """ Cleanup pending module resources """ + + @abstractmethod + def send_msg(self, msg: Dict[str, str]) -> None: + """ Sends a message to all registered rpc modules """ + + class RPC: """ RPC class can be used to have extra feature, like bot data, and access to DB data @@ -83,19 +109,6 @@ class RPC: if self._config.get('fiat_display_currency', None): self._fiat_converter = CryptoToFiatConverter() - @property - def name(self) -> str: - """ Returns the lowercase name of the implementation """ - return self.__class__.__name__.lower() - - @abstractmethod - def cleanup(self) -> None: - """ Cleanup pending module resources """ - - @abstractmethod - def send_msg(self, msg: Dict[str, str]) -> None: - """ Sends a message to all registered rpc modules """ - @staticmethod def _rpc_show_config(config, botstate: State) -> Dict[str, Any]: """ diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index c42878f99..38a4e95fd 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -4,7 +4,7 @@ This module contains class to manage RPC communications (Telegram, Slack, ...) import logging from typing import Any, Dict, List -from freqtrade.rpc import RPC, RPCMessageType +from freqtrade.rpc import RPC, RPCHandler, RPCMessageType logger = logging.getLogger(__name__) @@ -16,25 +16,26 @@ class RPCManager: """ def __init__(self, freqtrade) -> None: """ Initializes all enabled rpc modules """ - self.registered_modules: List[RPC] = [] - + self.registered_modules: List[RPCHandler] = [] + self._rpc = RPC(freqtrade) + config = freqtrade.config # Enable telegram - if freqtrade.config.get('telegram', {}).get('enabled', False): + if config.get('telegram', {}).get('enabled', False): logger.info('Enabling rpc.telegram ...') from freqtrade.rpc.telegram import Telegram - self.registered_modules.append(Telegram(freqtrade)) + self.registered_modules.append(Telegram(self._rpc, config)) # Enable Webhook - if freqtrade.config.get('webhook', {}).get('enabled', False): + if config.get('webhook', {}).get('enabled', False): logger.info('Enabling rpc.webhook ...') from freqtrade.rpc.webhook import Webhook - self.registered_modules.append(Webhook(freqtrade)) + self.registered_modules.append(Webhook(self._rpc, config)) # Enable local rest api server for cmd line control - if freqtrade.config.get('api_server', {}).get('enabled', False): + if config.get('api_server', {}).get('enabled', False): logger.info('Enabling rpc.api_server') from freqtrade.rpc.api_server import ApiServer - self.registered_modules.append(ApiServer(freqtrade)) + self.registered_modules.append(ApiServer(self._rpc, config)) def cleanup(self) -> None: """ Stops all enabled rpc modules """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e15071845..7ec67e5d0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -18,7 +18,7 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.exceptions import OperationalException -from freqtrade.rpc import RPC, RPCException, RPCMessageType +from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType logger = logging.getLogger(__name__) @@ -62,16 +62,18 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: return wrapper -class Telegram(RPC): +class Telegram(RPCHandler): """ This class handles all telegram communication """ - def __init__(self, freqtrade) -> None: + def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: + """ - Init the Telegram call, and init the super class RPC - :param freqtrade: Instance of a freqtrade bot + Init the Telegram call, and init the super class RPCHandler + :param rpc: instance of RPC Helper class + :param config: Configuration object :return: None """ - super().__init__(freqtrade) + super().__init__(rpc, config) self._updater: Updater self._init_keyboard() @@ -181,8 +183,8 @@ class Telegram(RPC): return if msg['type'] == RPCMessageType.BUY_NOTIFICATION: - if self._fiat_converter: - msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( + if self._rpc._fiat_converter: + msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) else: msg['stake_amount_fiat'] = 0 @@ -222,8 +224,8 @@ class Telegram(RPC): # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) - and self._fiat_converter): - msg['profit_fiat'] = self._fiat_converter.convert_amount( + and self._rpc._fiat_converter): + msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) message += (' `({gain}: {profit_amount:.8f} {stake_currency}' ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) @@ -275,7 +277,7 @@ class Telegram(RPC): return try: - results = self._rpc_trade_status() + results = self._rpc._rpc_trade_status() messages = [] for r in results: @@ -325,8 +327,9 @@ class Telegram(RPC): :return: None """ try: - statlist, head = self._rpc_status_table(self._config['stake_currency'], - self._config.get('fiat_display_currency', '')) + statlist, head = self._rpc._rpc_status_table( + self._config['stake_currency'], self._config.get('fiat_display_currency', '')) + message = tabulate(statlist, headers=head, tablefmt='simple') self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) except RPCException as e: @@ -348,7 +351,7 @@ class Telegram(RPC): except (TypeError, ValueError, IndexError): timescale = 7 try: - stats = self._rpc_daily_profit( + stats = self._rpc._rpc_daily_profit( timescale, stake_cur, fiat_disp_cur @@ -382,7 +385,7 @@ class Telegram(RPC): stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') - stats = self._rpc_trade_statistics( + stats = self._rpc._rpc_trade_statistics( stake_cur, fiat_disp_cur) profit_closed_coin = stats['profit_closed_coin'] @@ -433,7 +436,7 @@ class Telegram(RPC): Handler for /stats Show stats of recent trades """ - stats = self._rpc_stats() + stats = self._rpc._rpc_stats() reason_map = { 'roi': 'ROI', @@ -473,8 +476,8 @@ class Telegram(RPC): def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ try: - result = self._rpc_balance(self._config['stake_currency'], - self._config.get('fiat_display_currency', '')) + result = self._rpc._rpc_balance(self._config['stake_currency'], + self._config.get('fiat_display_currency', '')) output = '' if self._config['dry_run']: @@ -517,7 +520,7 @@ class Telegram(RPC): :param update: message update :return: None """ - msg = self._rpc_start() + msg = self._rpc._rpc_start() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only @@ -529,7 +532,7 @@ class Telegram(RPC): :param update: message update :return: None """ - msg = self._rpc_stop() + msg = self._rpc._rpc_stop() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only @@ -541,7 +544,7 @@ class Telegram(RPC): :param update: message update :return: None """ - msg = self._rpc_reload_config() + msg = self._rpc._rpc_reload_config() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only @@ -553,7 +556,7 @@ class Telegram(RPC): :param update: message update :return: None """ - msg = self._rpc_stopbuy() + msg = self._rpc._rpc_stopbuy() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only @@ -571,7 +574,7 @@ class Telegram(RPC): self._send_msg("You must specify a trade-id or 'all'.") return try: - msg = self._rpc_forcesell(trade_id) + msg = self._rpc._rpc_forcesell(trade_id) self._send_msg('Forcesell Result: `{result}`'.format(**msg)) except RPCException as e: @@ -590,7 +593,7 @@ class Telegram(RPC): pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None try: - self._rpc_forcebuy(pair, price) + self._rpc._rpc_forcebuy(pair, price) except RPCException as e: self._send_msg(str(e)) @@ -609,7 +612,7 @@ class Telegram(RPC): except (TypeError, ValueError, IndexError): nrecent = 10 try: - trades = self._rpc_trade_history( + trades = self._rpc._rpc_trade_history( nrecent ) trades_tab = tabulate( @@ -642,7 +645,7 @@ class Telegram(RPC): if not context.args or len(context.args) == 0: raise RPCException("Trade-id not set.") trade_id = int(context.args[0]) - msg = self._rpc_delete(trade_id) + msg = self._rpc._rpc_delete(trade_id) self._send_msg(( '`{result_msg}`\n' 'Please make sure to take care of this asset on the exchange manually.' @@ -661,7 +664,7 @@ class Telegram(RPC): :return: None """ try: - trades = self._rpc_performance() + trades = self._rpc._rpc_performance() stats = '\n'.join('{index}.\t{pair}\t{profit:.2f}% ({count})'.format( index=i + 1, pair=trade['pair'], @@ -683,7 +686,7 @@ class Telegram(RPC): :return: None """ try: - counts = self._rpc_count() + counts = self._rpc._rpc_count() message = tabulate({k: [v] for k, v in counts.items()}, headers=['current', 'max', 'total stake'], tablefmt='simple') @@ -700,7 +703,7 @@ class Telegram(RPC): Returns the currently active locks """ try: - locks = self._rpc_locks() + locks = self._rpc._rpc_locks() message = tabulate([[ lock['pair'], lock['lock_end_time'], @@ -720,7 +723,7 @@ class Telegram(RPC): Shows the currently active whitelist """ try: - whitelist = self._rpc_whitelist() + whitelist = self._rpc._rpc_whitelist() message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n" message += f"`{', '.join(whitelist['whitelist'])}`" @@ -738,7 +741,7 @@ class Telegram(RPC): """ try: - blacklist = self._rpc_blacklist(context.args) + blacklist = self._rpc._rpc_blacklist(context.args) errmsgs = [] for pair, error in blacklist['errors'].items(): errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`") @@ -792,7 +795,7 @@ class Telegram(RPC): Shows information related to Edge """ try: - edge_pairs = self._rpc_edge() + edge_pairs = self._rpc._rpc_edge() edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') message = f'Edge only validated following pairs:\n
{edge_pairs_tab}
' self._send_msg(message, parse_mode=ParseMode.HTML) @@ -862,7 +865,7 @@ class Telegram(RPC): :param update: message update :return: None """ - val = RPC._rpc_show_config(self._freqtrade.config, self._freqtrade.state) + val = RPC._rpc_show_config(self._config, self._rpc._freqtrade.state) if val['trailing_stop']: sl_info = ( diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index f4008a70f..5796201b5 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -6,7 +6,7 @@ from typing import Any, Dict from requests import RequestException, post -from freqtrade.rpc import RPC, RPCMessageType +from freqtrade.rpc import RPC, RPCHandler, RPCMessageType logger = logging.getLogger(__name__) @@ -14,16 +14,17 @@ logger = logging.getLogger(__name__) logger.debug('Included module rpc.webhook ...') -class Webhook(RPC): +class Webhook(RPCHandler): """ This class handles all webhook communication """ - def __init__(self, freqtrade) -> None: + def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: """ - Init the Webhook class, and init the super class RPC - :param freqtrade: Instance of a freqtrade bot + Init the Webhook class, and init the super class RPCHandler + :param rpc: instance of RPC Helper class + :param config: Configuration object :return: None """ - super().__init__(freqtrade) + super().__init__(rpc, config) self._url = self._config['webhook']['url'] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a1f4f7c9d..e7eee6f05 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -13,6 +13,7 @@ from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade +from freqtrade.rpc import RPC from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import RunMode, State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -36,8 +37,9 @@ def botclient(default_conf, mocker): }}) ftbot = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(ftbot) mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) - apiserver = ApiServer(ftbot) + apiserver = ApiServer(rpc, default_conf) yield ftbot, apiserver.app.test_client() # Cleanup ... ? @@ -179,8 +181,7 @@ def test_api__init__(default_conf, mocker): }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) - - apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) assert apiserver._config == default_conf @@ -197,7 +198,7 @@ def test_api_run(default_conf, mocker, caplog): server_mock = MagicMock() mocker.patch('freqtrade.rpc.api_server.make_server', server_mock) - apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) assert apiserver._config == default_conf apiserver.run() @@ -251,7 +252,7 @@ def test_api_cleanup(default_conf, mocker, caplog): mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) - apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) apiserver.run() stop_mock = MagicMock() stop_mock.shutdown = MagicMock() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5040f35cf..97b9e5e7c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -20,7 +20,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade -from freqtrade.rpc import RPCMessageType +from freqtrade.rpc import RPC, RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellType @@ -32,8 +32,8 @@ class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator """ - def __init__(self, freqtrade) -> None: - super().__init__(freqtrade) + def __init__(self, rpc: RPC, config) -> None: + super().__init__(rpc, config) self.state = {'called': False} def _init(self): @@ -54,7 +54,7 @@ class DummyCls(Telegram): raise Exception('test') -def get_telegram_testobject(mocker, default_conf, mock=True): +def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): msg_mock = MagicMock() if mock: mocker.patch.multiple( @@ -62,8 +62,10 @@ def get_telegram_testobject(mocker, default_conf, mock=True): _init=MagicMock(), _send_msg=msg_mock ) - ftbot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(ftbot) + if not ftbot: + ftbot = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(ftbot) + telegram = Telegram(rpc, default_conf) return telegram, ftbot, msg_mock @@ -112,8 +114,10 @@ def test_authorized_only(default_conf, mocker, caplog, update) -> None: default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) + rpc = RPC(bot) + dummy = DummyCls(rpc, default_conf) + patch_get_signal(bot, (True, False)) - dummy = DummyCls(bot) dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state['called'] is True assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog) @@ -129,8 +133,10 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) + rpc = RPC(bot) + dummy = DummyCls(rpc, default_conf) + patch_get_signal(bot, (True, False)) - dummy = DummyCls(bot) dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state['called'] is False assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog) @@ -144,8 +150,9 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) + rpc = RPC(bot) + dummy = DummyCls(rpc, default_conf) patch_get_signal(bot, (True, False)) - dummy = DummyCls(bot) dummy.dummy_exception(update=update, context=MagicMock()) assert dummy.state['called'] is False @@ -160,8 +167,10 @@ def test_telegram_status(default_conf, update, mocker) -> None: default_conf['telegram']['chat_id'] = "123" status_table = MagicMock() + mocker.patch('freqtrade.rpc.telegram.Telegram._status_table', status_table) + mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', + 'freqtrade.rpc.rpc.RPC', _rpc_trade_status=MagicMock(return_value=[{ 'trade_id': 1, 'pair': 'ETH/BTC', @@ -188,7 +197,6 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'open_order': '(limit buy rem=0.00000000)', 'is_open': True }]), - _status_table=status_table, ) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -642,8 +650,9 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, ) freqtradebot = FreqtradeBot(default_conf) + rpc = RPC(freqtradebot) + telegram = Telegram(rpc, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Create some test data freqtradebot.enter_positions() @@ -698,8 +707,9 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, ) freqtradebot = FreqtradeBot(default_conf) + rpc = RPC(freqtradebot) + telegram = Telegram(rpc, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Create some test data freqtradebot.enter_positions() @@ -756,8 +766,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None ) default_conf['max_open_trades'] = 4 freqtradebot = FreqtradeBot(default_conf) + rpc = RPC(freqtradebot) + telegram = Telegram(rpc, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Create some test data freqtradebot.enter_positions() @@ -1216,8 +1227,8 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - old_convamount = telegram._fiat_converter.convert_amount - telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 + old_convamount = telegram._rpc._fiat_converter.convert_amount + telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Binance', @@ -1274,15 +1285,15 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Profit:* `-57.41%`') # Reset singleton function to avoid random breaks - telegram._fiat_converter.convert_amount = old_convamount + telegram._rpc._fiat_converter.convert_amount = old_convamount def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - old_convamount = telegram._fiat_converter.convert_amount - telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 + old_convamount = telegram._rpc._fiat_converter.convert_amount + telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, 'exchange': 'Binance', @@ -1303,7 +1314,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] \ == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout') # Reset singleton function to avoid random breaks - telegram._fiat_converter.convert_amount = old_convamount + telegram._rpc._fiat_converter.convert_amount = old_convamount def test_send_msg_status_notification(default_conf, mocker) -> None: @@ -1449,6 +1460,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: bot = MagicMock() bot.send_message = MagicMock() freqtradebot = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(freqtradebot) invalid_keys_list = [['/not_valid', '/profit'], ['/daily'], ['/alsoinvalid']] default_keys_list = [['/daily', '/profit', '/balance'], @@ -1461,7 +1473,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) def init_telegram(freqtradebot): - telegram = Telegram(freqtradebot) + telegram = Telegram(rpc, default_conf) telegram._updater = MagicMock() telegram._updater.bot = bot return telegram diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 9256a5316..4ca547390 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest from requests import RequestException -from freqtrade.rpc import RPCMessageType +from freqtrade.rpc import RPC, RPCMessageType from freqtrade.rpc.webhook import Webhook from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has @@ -45,7 +45,7 @@ def get_webhook_dict() -> dict: def test__init__(mocker, default_conf): default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"} - webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) assert webhook._config == default_conf @@ -53,7 +53,7 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"] = get_webhook_dict() msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) - webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) # Test buy msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) @@ -172,7 +172,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() del default_conf["webhook"]["webhookbuy"] - webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks", caplog) @@ -181,7 +181,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}" msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) - webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', @@ -209,7 +209,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): def test__send_msg(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() - webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = {'value1': 'DEADBEEF', 'value2': 'ALIVEBEEF', 'value3': 'FREQTRADE'} From 7d2b9447d078969fd7c3ea04f7ba30ea780ce2af Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Dec 2020 15:30:55 +0100 Subject: [PATCH 056/563] Update slack link --- CONTRIBUTING.md | 2 +- README.md | 4 ++-- docs/developer.md | 2 +- docs/faq.md | 2 +- docs/index.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b4e8adaf..5c52a8e93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Few pointers for contributions: - New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. - PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). -If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. +If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index a9aee342f..1031e4d67 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ For any questions not covered by the documentation or for further information ab Please check out our [discord server](https://discord.gg/MA9v74M). -You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) @@ -169,7 +169,7 @@ to understand the requirements before sending your pull-requests. Coding is not a necessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. -**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Important:** Always create your PR against the `develop` branch, not `stable`. diff --git a/docs/developer.md b/docs/developer.md index dcbaa3ca9..07d686084 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,7 +2,7 @@ This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running. -All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) where you can ask questions. +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) where you can ask questions. ## Documentation diff --git a/docs/faq.md b/docs/faq.md index b424cd31d..5742f512a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -143,7 +143,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: diff --git a/docs/index.md b/docs/index.md index e6882263b..38e040d7a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,7 +65,7 @@ For any questions not covered by the documentation or for further information ab Please check out our [discord server](https://discord.gg/MA9v74M). -You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). ## Ready to try? From b3e929d14b1155b3d8de3372d0205c1ade3b49d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Dec 2020 05:37:11 +0000 Subject: [PATCH 057/563] Bump mkdocs-material from 6.1.7 to 6.2.3 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.7 to 6.2.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.7...6.2.3) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 2b133cb07..f14afefc1 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.7 +mkdocs-material==6.2.3 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From 10840ec170a0f0ad8b2e3e2532500d1c06b3055e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Dec 2020 05:37:13 +0000 Subject: [PATCH 058/563] Bump pymdown-extensions from 8.0.1 to 8.1 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 8.0.1 to 8.1. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/8.0.1...8.1) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 2b133cb07..a7f5a1d47 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ mkdocs-material==6.1.7 mdx_truly_sane_lists==1.2 -pymdown-extensions==8.0.1 +pymdown-extensions==8.1 From 87b896879f4d142eea72936ecf1caf9698bc90f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Dec 2020 05:37:13 +0000 Subject: [PATCH 059/563] Bump ccxt from 1.39.52 to 1.39.79 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.39.52 to 1.39.79. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.39.52...1.39.79) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2c565fee5..c265d6ffe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.5 -ccxt==1.39.52 +ccxt==1.39.79 aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 From 092ebf845d2e27a80d0f2cdcf3c55c771b6f5742 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Dec 2020 05:37:16 +0000 Subject: [PATCH 060/563] Bump scikit-learn from 0.23.2 to 0.24.0 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 0.23.2 to 0.24.0. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/0.23.2...0.24.0) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index c51062bf7..a2446ddb8 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.5.4 -scikit-learn==0.23.2 +scikit-learn==0.24.0 scikit-optimize==0.8.1 filelock==3.0.12 joblib==1.0.0 From f49260911595f55183f1bdc28ac002f28ee4914f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Dec 2020 05:37:17 +0000 Subject: [PATCH 061/563] Bump blosc from 1.9.2 to 1.10.1 Bumps [blosc](https://github.com/blosc/python-blosc) from 1.9.2 to 1.10.1. - [Release notes](https://github.com/blosc/python-blosc/releases) - [Changelog](https://github.com/Blosc/python-blosc/blob/master/RELEASE_NOTES.rst) - [Commits](https://github.com/blosc/python-blosc/compare/v1.9.2...v1.10.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2c565fee5..a5d4a0979 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ tabulate==0.8.7 pycoingecko==1.4.0 jinja2==2.11.2 tables==3.6.1 -blosc==1.9.2 +blosc==1.10.1 # find first, C search in arrays py_find_1st==1.1.4 From 30087697e06c5e64309e891b8ab2c06f78246c10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Dec 2020 08:21:17 +0000 Subject: [PATCH 062/563] Bump pandas from 1.1.5 to 1.2.0 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.1.5 to 1.2.0. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.1.5...v1.2.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 594c22b74..bab13ed03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.19.4 -pandas==1.1.5 +pandas==1.2.0 ccxt==1.39.79 aiohttp==3.7.3 From 0d4cf32086f3d9942fb2ae0517da464b5b517457 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 09:50:48 +0100 Subject: [PATCH 063/563] Slightly adapt to pandas incompatibility --- freqtrade/data/btanalysis.py | 2 +- tests/rpc/test_rpc_apiserver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 513fba9e7..2b51f5371 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -347,7 +347,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, # Resample to timeframe to make sure trades match candles _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_date' )[['profit_percent']].sum() - df.loc[:, col_name] = _trades_sum.cumsum() + df.loc[:, col_name] = _trades_sum['profit_percent'].cumsum() # Set first value to 0 df.loc[df.iloc[0].name, col_name] = 0 # FFill to get continuous diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e7eee6f05..5e608fb25 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -929,7 +929,7 @@ def test_api_pair_candles(botclient, ohlcv_history): ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None], ['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, - 0.7039405, 8.885000000000002e-05, 0, 0, 1511686800000, None, None] + 0.7039405, 8.885e-05, 0, 0, 1511686800000, None, None] ]) From f80ffe279ba45b526ce10cfc147ecb3bf3a9ae3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 09:54:58 +0100 Subject: [PATCH 064/563] Version bump 2020.12 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 3054bc4a1..170f95015 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.11' +__version__ = '2020.12' if __version__ == 'develop': From 003552d78c3ef426f42bcb132645842b763be569 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 10:19:24 +0100 Subject: [PATCH 065/563] Remove custom header section from docs --- docs/partials/header.html | 54 --------------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 docs/partials/header.html diff --git a/docs/partials/header.html b/docs/partials/header.html deleted file mode 100644 index 32202bccc..000000000 --- a/docs/partials/header.html +++ /dev/null @@ -1,54 +0,0 @@ -
- - - - -
From 8366e67fee163a1d06ce0d48e310ace200dbc410 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 10:19:24 +0100 Subject: [PATCH 066/563] Remove custom header section from docs --- docs/partials/header.html | 54 --------------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 docs/partials/header.html diff --git a/docs/partials/header.html b/docs/partials/header.html deleted file mode 100644 index 32202bccc..000000000 --- a/docs/partials/header.html +++ /dev/null @@ -1,54 +0,0 @@ -
- - - - -
From accc59aa1b90efbb7a1784da9da2ecc6739a88f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 13:49:08 +0100 Subject: [PATCH 067/563] Reinstate jquery --- docs/overrides/main.html | 8 ++++++++ mkdocs.yml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 docs/overrides/main.html diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 000000000..916d26770 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block site_meta %} + + + +{% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index a7ae0cc96..a0b5d8641 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,7 +38,7 @@ nav: theme: name: material logo: 'images/logo.png' - custom_dir: 'docs' + custom_dir: 'docs/overrides' palette: primary: 'blue grey' accent: 'tear' From ecea6c95263bbedc790c62fd0258009321cd64fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 14:02:30 +0100 Subject: [PATCH 068/563] Move jquery to the bottom --- docs/overrides/main.html | 4 ++-- mkdocs.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 916d26770..910af0973 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,8 +1,8 @@ {% extends "base.html" %} -{% block site_meta %} +{% block scripts %} - {% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index a0b5d8641..a14c67b03 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,7 @@ nav: theme: name: material logo: 'images/logo.png' + favicon: 'images/logo.png' custom_dir: 'docs/overrides' palette: primary: 'blue grey' From dcc7d559ee6edbf82b45e5f1596f2c1281ae5c35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 14:08:57 +0100 Subject: [PATCH 069/563] Reinstate header partials --- docs/overrides/main.html | 8 ------ docs/partials/header.html | 51 +++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 +- 3 files changed, 52 insertions(+), 9 deletions(-) delete mode 100644 docs/overrides/main.html create mode 100644 docs/partials/header.html diff --git a/docs/overrides/main.html b/docs/overrides/main.html deleted file mode 100644 index 910af0973..000000000 --- a/docs/overrides/main.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} - -{% block scripts %} - - - -{% endblock %} diff --git a/docs/partials/header.html b/docs/partials/header.html new file mode 100644 index 000000000..f5243225b --- /dev/null +++ b/docs/partials/header.html @@ -0,0 +1,51 @@ +{#- +This file was automatically generated - do not edit +-#} +{% set site_url = config.site_url | d(nav.homepage.url, true) | url %} +{% if not config.use_directory_urls and site_url[0] == site_url[-1] == "." %} +{% set site_url = site_url ~ "/index.html" %} +{% endif %} +
+ + + + +
diff --git a/mkdocs.yml b/mkdocs.yml index a14c67b03..96cfa7651 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,7 +39,7 @@ theme: name: material logo: 'images/logo.png' favicon: 'images/logo.png' - custom_dir: 'docs/overrides' + custom_dir: 'docs' palette: primary: 'blue grey' accent: 'tear' From 625da69fcbabbd9c7774c38b9526e5ecc6b00aa3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 10:19:24 +0100 Subject: [PATCH 070/563] Remove custom header section from docs From f6e56027b13c583351a8b0af9e49ef80a87aec6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 13:49:08 +0100 Subject: [PATCH 071/563] Reinstate jquery --- docs/overrides/main.html | 8 ++++++++ mkdocs.yml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 docs/overrides/main.html diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 000000000..916d26770 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block site_meta %} + + + +{% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index a7ae0cc96..a0b5d8641 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,7 +38,7 @@ nav: theme: name: material logo: 'images/logo.png' - custom_dir: 'docs' + custom_dir: 'docs/overrides' palette: primary: 'blue grey' accent: 'tear' From a2fdb9d2f6210c89e37c9f1bbbbaf22213e0b42f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 14:02:30 +0100 Subject: [PATCH 072/563] Move jquery to the bottom --- docs/overrides/main.html | 4 ++-- mkdocs.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 916d26770..910af0973 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,8 +1,8 @@ {% extends "base.html" %} -{% block site_meta %} +{% block scripts %} - {% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index a0b5d8641..a14c67b03 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,7 @@ nav: theme: name: material logo: 'images/logo.png' + favicon: 'images/logo.png' custom_dir: 'docs/overrides' palette: primary: 'blue grey' From 0925a3cd19290b075e5ef504ecee041f215ba190 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 14:08:57 +0100 Subject: [PATCH 073/563] Reinstate header partials --- docs/overrides/main.html | 8 ------ docs/partials/header.html | 51 +++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 +- 3 files changed, 52 insertions(+), 9 deletions(-) delete mode 100644 docs/overrides/main.html create mode 100644 docs/partials/header.html diff --git a/docs/overrides/main.html b/docs/overrides/main.html deleted file mode 100644 index 910af0973..000000000 --- a/docs/overrides/main.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} - -{% block scripts %} - - - -{% endblock %} diff --git a/docs/partials/header.html b/docs/partials/header.html new file mode 100644 index 000000000..f5243225b --- /dev/null +++ b/docs/partials/header.html @@ -0,0 +1,51 @@ +{#- +This file was automatically generated - do not edit +-#} +{% set site_url = config.site_url | d(nav.homepage.url, true) | url %} +{% if not config.use_directory_urls and site_url[0] == site_url[-1] == "." %} +{% set site_url = site_url ~ "/index.html" %} +{% endif %} +
+ + + + +
diff --git a/mkdocs.yml b/mkdocs.yml index a14c67b03..96cfa7651 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,7 +39,7 @@ theme: name: material logo: 'images/logo.png' favicon: 'images/logo.png' - custom_dir: 'docs/overrides' + custom_dir: 'docs' palette: primary: 'blue grey' accent: 'tear' From 238e9aabb1788b42d096ceb1a5c24d49dbadb1a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Dec 2020 20:05:07 +0100 Subject: [PATCH 074/563] Add test showing wrong behaviour --- tests/exchange/test_kraken.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 3803658eb..97f428e2f 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -89,6 +89,7 @@ def test_get_balances_prod(default_conf, mocker): '2ST': balance_item.copy(), '3ST': balance_item.copy(), '4ST': balance_item.copy(), + 'EUR': balance_item.copy(), }) kraken_open_orders = [{'symbol': '1ST/EUR', 'type': 'limit', @@ -123,21 +124,22 @@ def test_get_balances_prod(default_conf, mocker): 'remaining': 2.0, }, {'status': 'open', - 'symbol': 'BTC/3ST', + 'symbol': '3ST/EUR', 'type': 'limit', 'side': 'buy', - 'price': 20, + 'price': 0.02, 'cost': 0.0, - 'amount': 3.0, + 'amount': 100.0, 'filled': 0.0, 'average': 0.0, - 'remaining': 3.0, + 'remaining': 100.0, }] api_mock.fetch_open_orders = MagicMock(return_value=kraken_open_orders) default_conf['dry_run'] = False exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") balances = exchange.get_balances() - assert len(balances) == 4 + assert len(balances) == 5 + assert balances['1ST']['free'] == 9.0 assert balances['1ST']['total'] == 10.0 assert balances['1ST']['used'] == 1.0 @@ -146,13 +148,17 @@ def test_get_balances_prod(default_conf, mocker): assert balances['2ST']['total'] == 10.0 assert balances['2ST']['used'] == 4.0 - assert balances['3ST']['free'] == 7.0 + assert balances['3ST']['free'] == 10.0 assert balances['3ST']['total'] == 10.0 - assert balances['3ST']['used'] == 3.0 + assert balances['3ST']['used'] == 0.0 assert balances['4ST']['free'] == 10.0 assert balances['4ST']['total'] == 10.0 assert balances['4ST']['used'] == 0.0 + + assert balances['EUR']['free'] == 8.0 + assert balances['EUR']['total'] == 10.0 + assert balances['EUR']['used'] == 2.0 ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "get_balances", "fetch_balance") From b607740dd10d41394985715a70393a2ddc261dac Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Dec 2020 20:06:37 +0100 Subject: [PATCH 075/563] Fix kraken balance bug if open buy orders exist --- freqtrade/exchange/kraken.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 6dbb751e5..724b11189 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -48,7 +48,7 @@ class Kraken(Exchange): orders = self._api.fetch_open_orders() order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1], - x["remaining"], + x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"], # Don't remove the below comment, this can be important for debuggung # x["side"], x["amount"], ) for x in orders] From b8899b39ec2aacd7be38de76edc336520e72dfe1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Dec 2020 06:29:59 +0100 Subject: [PATCH 076/563] Show advanced plot-config section again closes #4132 --- docs/plotting.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plotting.md b/docs/plotting.md index ed682e44b..19ddb4f57 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -208,6 +208,7 @@ Sample configuration with inline comments explaining the process: } ``` + !!! Note The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, `macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy. From 2fdda8e448a5147f0abef0ba0fe26911909e8572 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Dec 2020 08:30:41 +0100 Subject: [PATCH 077/563] plot-profit should fail gracefully if no trade is within the selected timerange closes #4119 --- freqtrade/plot/plotting.py | 2 ++ tests/test_plotting.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 497218deb..40e3da9c9 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -444,6 +444,8 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], # Trim trades to available OHLCV data trades = extract_trades_of_period(df_comb, trades, date_index=True) + if len(trades) == 0: + raise OperationalException('No trades found in selected timerange.') # Add combined cumulative profit df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 42847ca50..8e7b0ef7c 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -353,6 +353,10 @@ def test_generate_profit_graph(testdatadir): profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}") assert isinstance(profit_pair, go.Scatter) + with pytest.raises(OperationalException, match=r"No trades found.*"): + # Pair cannot be empty - so it's an empty dataframe. + generate_profit_graph(pairs, data, trades.loc[trades['pair'].isnull()], timeframe="5m") + def test_start_plot_dataframe(mocker): aup = mocker.patch("freqtrade.plot.plotting.load_and_plot_trades", MagicMock()) From 704cf143835fb6bb1e12fb2a3b611415894f0126 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Dec 2020 09:55:44 +0100 Subject: [PATCH 078/563] Add expand_pairlist method --- freqtrade/exchange/exchange.py | 2 +- .../plugins/pairlist/pairlist_helpers.py | 18 +++++++++++ freqtrade/plugins/pairlistmanager.py | 10 +++++- tests/plugins/test_pairlist.py | 32 +++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 freqtrade/plugins/pairlist/pairlist_helpers.py diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6f495e605..11a0ef8e6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -208,7 +208,7 @@ class Exchange: return self._api.precisionMode def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None, - pairs_only: bool = False, active_only: bool = False) -> Dict: + pairs_only: bool = False, active_only: bool = False) -> Dict[str, Any]: """ Return exchange ccxt markets, filtered out by base currency and quote currency if this was requested in parameters. diff --git a/freqtrade/plugins/pairlist/pairlist_helpers.py b/freqtrade/plugins/pairlist/pairlist_helpers.py new file mode 100644 index 000000000..7d365a344 --- /dev/null +++ b/freqtrade/plugins/pairlist/pairlist_helpers.py @@ -0,0 +1,18 @@ +import re +from typing import List + + +def expand_pairlist(wildcardpl: List[str], available_pairs: List[str]) -> List[str]: + """ + TODO: Add docstring here + """ + result = [] + for pair_wc in wildcardpl: + try: + comp = re.compile(pair_wc) + result += [ + pair for pair in available_pairs if re.match(comp, pair) + ] + except re.error as err: + raise ValueError(f"Wildcard error in {pair_wc}, {err}") + return result diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index b71f02898..ea1f4ecc7 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -1,6 +1,7 @@ """ PairList manager class """ +from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist import logging from copy import deepcopy from typing import Any, Dict, List @@ -55,6 +56,13 @@ class PairListManager(): """ return self._blacklist + @property + def expanded_blacklist(self) -> List[str]: + """ + Has the expanded blacklist (including wildcard expansion) + """ + return expand_pairlist(self._blacklist, self._exchange.get_markets().keys()) + @property def name_list(self) -> List[str]: """ @@ -121,7 +129,7 @@ class PairListManager(): :return: pairlist - blacklisted pairs """ for pair in deepcopy(pairlist): - if pair in self._blacklist: + if pair in self.expanded_blacklist: logmethod(f"Pair {pair} in your blacklist. Removing it from whitelist...") pairlist.remove(pair) return pairlist diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 1795fc27f..25597ef93 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -6,6 +6,7 @@ import pytest from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.exceptions import OperationalException +from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver from tests.conftest import get_patched_freqtradebot, log_has, log_has_re @@ -804,3 +805,34 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o freqtrade.pairlists.refresh_pairlist() allowlist = freqtrade.pairlists.whitelist assert allowlist == allowlist_result + + +@pytest.mark.parametrize('wildcardlist,pairs,expected', [ + (['BTC/USDT'], + ['BTC/USDT'], + ['BTC/USDT']), + (['BTC/USDT', 'ETH/USDT'], + ['BTC/USDT', 'ETH/USDT'], + ['BTC/USDT', 'ETH/USDT']), + (['BTC/USDT', 'ETH/USDT'], + ['BTC/USDT'], ['BTC/USDT']), # Test one too many + (['.*/USDT'], + ['BTC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETH/USDT']), # Wildcard simple + (['.*C/USDT'], + ['BTC/USDT', 'ETC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETC/USDT']), # Wildcard exclude one + (['.*UP/USDT', 'BTC/USDT', 'ETH/USDT'], + ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'], + ['BTC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT']), # Wildcard exclude one + (['BTC/.*', 'ETH/.*'], + ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP'], + ['BTC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP']), # Wildcard exclude one + (['*UP/USDT', 'BTC/USDT', 'ETH/USDT'], + ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'], + None), +]) +def test_expand_pairlist(wildcardlist, pairs, expected): + if expected is None: + with pytest.raises(ValueError, match=r'Wildcard error in \*UP/USDT,'): + expand_pairlist(wildcardlist, pairs) + else: + assert sorted(expand_pairlist(wildcardlist, pairs)) == sorted(expected) From 9feabe707fc501086a0cfee59799be8d6296f62f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Dec 2020 09:57:31 +0100 Subject: [PATCH 079/563] Fix RPC methods to allow wildcards (and validate wildcards) --- freqtrade/rpc/rpc.py | 17 +++++++++-------- tests/rpc/test_rpc.py | 14 ++++++++++++-- tests/rpc/test_rpc_apiserver.py | 13 +++++++++++++ tests/rpc/test_rpc_telegram.py | 12 +++++------- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 42ab76622..70a99a186 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -20,6 +20,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler from freqtrade.misc import shorten_date from freqtrade.persistence import PairLocks, Trade +from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State from freqtrade.strategy.interface import SellType @@ -673,23 +674,23 @@ class RPC: """ Returns the currently active blacklist""" errors = {} if add: - stake_currency = self._freqtrade.config.get('stake_currency') for pair in add: - if self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency: - if pair not in self._freqtrade.pairlists.blacklist: + if pair not in self._freqtrade.pairlists.blacklist: + try: + expand_pairlist([pair], self._freqtrade.exchange.get_markets().keys()) self._freqtrade.pairlists.blacklist.append(pair) - else: - errors[pair] = { - 'error_msg': f'Pair {pair} already in pairlist.'} + except ValueError: + errors[pair] = { + 'error_msg': f'Pair {pair} is not a valid wildcard.'} else: errors[pair] = { - 'error_msg': f"Pair {pair} does not match stake currency." - } + 'error_msg': f'Pair {pair} already in pairlist.'} res = {'method': self._freqtrade.pairlists.name_list, 'length': len(self._freqtrade.pairlists.blacklist), 'blacklist': self._freqtrade.pairlists.blacklist, + 'blacklist_expanded': self._freqtrade.pairlists.expanded_blacklist, 'errors': errors, } return res diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 19788c067..8ec356d54 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -957,14 +957,24 @@ def test_rpc_blacklist(mocker, default_conf) -> None: assert isinstance(ret['errors'], dict) assert ret['errors']['ETH/BTC']['error_msg'] == 'Pair ETH/BTC already in pairlist.' - ret = rpc._rpc_blacklist(["ETH/ETH"]) + ret = rpc._rpc_blacklist(["*/BTC"]) assert 'StaticPairList' in ret['method'] assert len(ret['blacklist']) == 3 assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC'] + assert ret['blacklist_expanded'] == ['ETH/BTC'] + assert 'errors' in ret + assert isinstance(ret['errors'], dict) + assert ret['errors'] == {'*/BTC': {'error_msg': 'Pair */BTC is not a valid wildcard.'}} + + ret = rpc._rpc_blacklist(["XRP/.*"]) + assert 'StaticPairList' in ret['method'] + assert len(ret['blacklist']) == 4 + assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] + assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC', 'XRP/.*'] + assert ret['blacklist_expanded'] == ['ETH/BTC', 'XRP/BTC'] assert 'errors' in ret assert isinstance(ret['errors'], dict) - assert ret['errors']['ETH/ETH']['error_msg'] == 'Pair ETH/ETH does not match stake currency.' def test_rpc_edge_disabled(mocker, default_conf) -> None: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5e608fb25..8da4ebfe7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -730,7 +730,9 @@ def test_api_blacklist(botclient, mocker): rc = client_get(client, f"{BASE_URI}/blacklist") assert_response(rc) + # DOGE and HOT are not in the markets mock! assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], + "blacklist_expanded": [], "length": 2, "method": ["StaticPairList"], "errors": {}, @@ -741,11 +743,22 @@ def test_api_blacklist(botclient, mocker): data='{"blacklist": ["ETH/BTC"]}') assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], + "blacklist_expanded": ["ETH/BTC"], "length": 3, "method": ["StaticPairList"], "errors": {}, } + rc = client_post(client, f"{BASE_URI}/blacklist", + data='{"blacklist": ["XRP/.*"]}') + assert_response(rc) + assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"], + "blacklist_expanded": ["ETH/BTC", "XRP/BTC"], + "length": 4, + "method": ["StaticPairList"], + "errors": {}, + } + def test_api_whitelist(botclient): ftbot, client = botclient diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 97b9e5e7c..a21a19e3a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1011,15 +1011,13 @@ def test_blacklist_static(default_conf, update, mocker) -> None: msg_mock.reset_mock() context = MagicMock() - context.args = ["ETH/ETH"] + context.args = ["XRP/.*"] telegram._blacklist(update=update, context=context) - assert msg_mock.call_count == 2 - assert ("Error adding `ETH/ETH` to blacklist: `Pair ETH/ETH does not match stake currency.`" - in msg_mock.call_args_list[0][0][0]) + assert msg_mock.call_count == 1 - assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`" - in msg_mock.call_args_list[1][0][0]) - assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"] + assert ("Blacklist contains 4 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC, XRP/.*`" + in msg_mock.call_args_list[0][0][0]) + assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"] def test_telegram_logs(default_conf, update, mocker) -> None: From 0affacd39aa808d60d130b0518eaf698c53399ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Dec 2020 10:14:22 +0100 Subject: [PATCH 080/563] Support invalid regex blacklist from config --- freqtrade/plugins/pairlist/pairlist_helpers.py | 7 ++++++- freqtrade/plugins/pairlistmanager.py | 7 ++++++- tests/plugins/test_pairlist.py | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/pairlist_helpers.py b/freqtrade/plugins/pairlist/pairlist_helpers.py index 7d365a344..0a0812d6a 100644 --- a/freqtrade/plugins/pairlist/pairlist_helpers.py +++ b/freqtrade/plugins/pairlist/pairlist_helpers.py @@ -4,7 +4,12 @@ from typing import List def expand_pairlist(wildcardpl: List[str], available_pairs: List[str]) -> List[str]: """ - TODO: Add docstring here + Expand pairlist potentially containing wildcards based on available markets. + This will implicitly filter all pairs in the wildcard-list which are not in available_pairs. + :param wildcardpl: List of Pairlists, which may contain regex + :param available_pairs: List of all available pairs, usually with `exchange.get_markets().keys()` + :return expanded pairlist, with Regexes from wildcardpl applied to match all available pairs. + :raises: ValueError if a wildcard is invalid (like '*/BTC' - which should be `.*/BTC`) """ result = [] for pair_wc in wildcardpl: diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index ea1f4ecc7..a0b8c63bc 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -128,8 +128,13 @@ class PairListManager(): :param logmethod: Function that'll be called, `logger.info` or `logger.warning`. :return: pairlist - blacklisted pairs """ + try: + blacklist = self.expanded_blacklist + except ValueError as err: + logger.error(f"Pair blacklist contains an invalid Wildcard: {err}") + return [] for pair in deepcopy(pairlist): - if pair in self.expanded_blacklist: + if pair in blacklist: logmethod(f"Pair {pair} in your blacklist. Removing it from whitelist...") pairlist.remove(pair) return pairlist diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 25597ef93..d822f8319 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -156,6 +156,23 @@ def test_refresh_static_pairlist(mocker, markets, static_pl_conf): assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist +def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog): + static_pl_conf['exchange']['pair_blacklist'] = ['*/BTC'] + freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + exchange_has=MagicMock(return_value=True), + markets=PropertyMock(return_value=markets), + ) + freqtrade.pairlists.refresh_pairlist() + # List ordered by BaseVolume + whitelist = [] + # Ensure all except those in whitelist are removed + assert set(whitelist) == set(freqtrade.pairlists.whitelist) + assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist + log_has_re(r"Pair blacklist contains an invalid Wildcard.*", caplog) + + def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf): mocker.patch.multiple( From 04624aae40d9205c56e5b07547eb427eeab540f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Dec 2020 10:21:05 +0100 Subject: [PATCH 081/563] Add documentation for wildcard-blacklist --- docs/includes/pairlists.md | 8 ++++++++ freqtrade/plugins/pairlist/pairlist_helpers.py | 2 +- freqtrade/plugins/pairlistmanager.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 732dfa5bb..82e578484 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -10,6 +10,14 @@ If multiple Pairlist Handlers are used, they are chained and a combination of al Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist. +### Pair blacklist + +The pair blacklist (configured via `exchange.pair_blacklist` in the configuration) disallows certain pairs from trading. +This can be as simple as excluding `DOGE/BTC` - which will remove exactly this pair. + +The pair-blacklist does also support wildcards (in regex-style) - so `BNB/.*` will exclude ALL pairs that start with BNB. +You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged tokens (check Naming conventions for your exchange!) + ### Available Pairlist Handlers * [`StaticPairList`](#static-pair-list) (default, if not configured differently) diff --git a/freqtrade/plugins/pairlist/pairlist_helpers.py b/freqtrade/plugins/pairlist/pairlist_helpers.py index 0a0812d6a..3352777f0 100644 --- a/freqtrade/plugins/pairlist/pairlist_helpers.py +++ b/freqtrade/plugins/pairlist/pairlist_helpers.py @@ -7,7 +7,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str]) -> List[s Expand pairlist potentially containing wildcards based on available markets. This will implicitly filter all pairs in the wildcard-list which are not in available_pairs. :param wildcardpl: List of Pairlists, which may contain regex - :param available_pairs: List of all available pairs, usually with `exchange.get_markets().keys()` + :param available_pairs: List of all available pairs (`exchange.get_markets().keys()`) :return expanded pairlist, with Regexes from wildcardpl applied to match all available pairs. :raises: ValueError if a wildcard is invalid (like '*/BTC' - which should be `.*/BTC`) """ diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index a0b8c63bc..867c07736 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -1,7 +1,6 @@ """ PairList manager class """ -from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist import logging from copy import deepcopy from typing import Any, Dict, List @@ -11,6 +10,7 @@ from cachetools import TTLCache, cached from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.resolvers import PairListResolver From bd7600ff0673137a1377d0ac704c180f1dc707bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Dec 2020 09:43:24 +0100 Subject: [PATCH 082/563] Small visual changes --- docs/includes/pairlists.md | 2 +- freqtrade/plugins/pairlistmanager.py | 22 ++++++---------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 82e578484..8919c4e3d 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -16,7 +16,7 @@ The pair blacklist (configured via `exchange.pair_blacklist` in the configuratio This can be as simple as excluding `DOGE/BTC` - which will remove exactly this pair. The pair-blacklist does also support wildcards (in regex-style) - so `BNB/.*` will exclude ALL pairs that start with BNB. -You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged tokens (check Naming conventions for your exchange!) +You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged tokens (check Pair naming conventions for your exchange!) ### Available Pairlist Handlers diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 867c07736..ad7b46cb8 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -43,37 +43,29 @@ class PairListManager(): @property def whitelist(self) -> List[str]: - """ - Has the current whitelist - """ + """The current whitelist""" return self._whitelist @property def blacklist(self) -> List[str]: """ - Has the current blacklist + The current blacklist -> no need to overwrite in subclasses """ return self._blacklist @property def expanded_blacklist(self) -> List[str]: - """ - Has the expanded blacklist (including wildcard expansion) - """ + """The expanded blacklist (including wildcard expansion)""" return expand_pairlist(self._blacklist, self._exchange.get_markets().keys()) @property def name_list(self) -> List[str]: - """ - Get list of loaded Pairlist Handler names - """ + """Get list of loaded Pairlist Handler names""" return [p.name for p in self._pairlist_handlers] def short_desc(self) -> List[Dict]: - """ - List of short_desc for each Pairlist Handler - """ + """List of short_desc for each Pairlist Handler""" return [{p.name: p.short_desc()} for p in self._pairlist_handlers] @cached(TTLCache(maxsize=1, ttl=1800)) @@ -81,9 +73,7 @@ class PairListManager(): return self._exchange.get_tickers() def refresh_pairlist(self) -> None: - """ - Run pairlist through all configured Pairlist Handlers. - """ + """Run pairlist through all configured Pairlist Handlers.""" # Tickers should be cached to avoid calling the exchange on each call. tickers: Dict = {} if self._tickers_needed: From 512e1633556ce9cdb4b8afed6205f36fb33feb7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Dec 2020 09:48:49 +0100 Subject: [PATCH 083/563] change docstring to better reflect what the method is for --- docs/strategy-advanced.md | 2 +- freqtrade/strategy/interface.py | 2 +- freqtrade/templates/subtemplates/strategy_methods_advanced.j2 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 49720d729..519a9ac62 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -37,7 +37,7 @@ To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum re When not implemented by a strategy, returns the initial stoploss value Only called when use_custom_stoploss is set to True. - :param pair: Pair that's about to be sold. + :param pair: Pair that's currently analyzed :param trade: trade object. :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 61c879e8f..1e4fc8b12 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -267,7 +267,7 @@ class IStrategy(ABC): When not implemented by a strategy, returns the initial stoploss value Only called when use_custom_stoploss is set to True. - :param pair: Pair that's about to be sold. + :param pair: Pair that's currently analyzed :param trade: trade object. :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index f4cda2e89..53ededa19 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -69,7 +69,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: When not implemented by a strategy, returns True (always confirming). - :param pair: Pair that's about to be sold. + :param pair: Pair that's currently analyzed :param trade: trade object. :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in quote currency. From e5840abaf9f77ecb47df29ca352f07ec0e4ae032 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Thu, 31 Dec 2020 21:05:47 +0100 Subject: [PATCH 084/563] Added imports to documentation for clarification when using custom stoploss Signed-off-by: hoeckxer --- docs/strategy-advanced.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 519a9ac62..7876106be 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -23,6 +23,10 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: ``` python +# additional imports required +from freqtrade.persistence import Trade +from datetime import datetime + use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, From 67ced6a53c78a291c31b62e551cb961449b6ea83 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Fri, 1 Jan 2021 20:49:04 +0100 Subject: [PATCH 085/563] Update docs/strategy-advanced.md Co-authored-by: Matthias --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7876106be..ab94be1c4 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -29,7 +29,7 @@ from datetime import datetime use_custom_stoploss = True - def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). From 11f36fbaee6d72580cc649635f71ee635c45af8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Jan 2021 09:14:31 +0100 Subject: [PATCH 086/563] Fix all custom stoploss samples --- docs/strategy-advanced.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index ab94be1c4..b1fcb50fc 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -24,8 +24,12 @@ To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum re ``` python # additional imports required -from freqtrade.persistence import Trade from datetime import datetime +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods use_custom_stoploss = True @@ -70,6 +74,13 @@ Of course, many more things are possible, and all examples can be combined at wi Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. ``` python +from datetime import datetime, timedelta +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, @@ -89,6 +100,13 @@ Use a different stoploss depending on the pair. In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs. ``` python +from datetime import datetime +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, @@ -111,6 +129,13 @@ The below example sets absolute profit levels based on the current profit. * Once profit is > 20% - stoploss will be set to 7%. ``` python +from datetime import datetime +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, From 1e38fec61b42c998111297a56b8e2c7b2a304919 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 06:55:19 +0100 Subject: [PATCH 087/563] Initial fastapi implementation (Ping working) --- freqtrade/rpc/api_server2/__init__.py | 2 + freqtrade/rpc/api_server2/api_v1.py | 14 +++++ freqtrade/rpc/api_server2/uvicorn_threaded.py | 28 ++++++++++ freqtrade/rpc/api_server2/webserver.py | 56 +++++++++++++++++++ freqtrade/rpc/rpc_manager.py | 5 +- requirements.txt | 4 ++ 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 freqtrade/rpc/api_server2/__init__.py create mode 100644 freqtrade/rpc/api_server2/api_v1.py create mode 100644 freqtrade/rpc/api_server2/uvicorn_threaded.py create mode 100644 freqtrade/rpc/api_server2/webserver.py diff --git a/freqtrade/rpc/api_server2/__init__.py b/freqtrade/rpc/api_server2/__init__.py new file mode 100644 index 000000000..df255c186 --- /dev/null +++ b/freqtrade/rpc/api_server2/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +from .webserver import ApiServer diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py new file mode 100644 index 000000000..1e8bae1d4 --- /dev/null +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -0,0 +1,14 @@ +from typing import Dict + +from fastapi import APIRouter + + +router = APIRouter() + + +@router.get('/ping') +def _ping() -> Dict[str, str]: + """simple ping version""" + return {"status": "pong"} + + diff --git a/freqtrade/rpc/api_server2/uvicorn_threaded.py b/freqtrade/rpc/api_server2/uvicorn_threaded.py new file mode 100644 index 000000000..ba9263620 --- /dev/null +++ b/freqtrade/rpc/api_server2/uvicorn_threaded.py @@ -0,0 +1,28 @@ +import contextlib +import time +import threading +import uvicorn + + +class UvicornServer(uvicorn.Server): + """ + Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742 + """ + def install_signal_handlers(self): + pass + + @contextlib.contextmanager + def run_in_thread(self): + self.thread = threading.Thread(target=self.run) + self.thread.start() + # try: + while not self.started: + time.sleep(1e-3) + # yield + # finally: + # self.should_exit = True + # thread.join() + + def cleanup(self): + self.should_exit = True + self.thread.join() diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py new file mode 100644 index 000000000..3f9baeaff --- /dev/null +++ b/freqtrade/rpc/api_server2/webserver.py @@ -0,0 +1,56 @@ +import threading +from typing import Any, Dict + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +import uvicorn + +from freqtrade.rpc.fiat_convert import CryptoToFiatConverter +from freqtrade.rpc.rpc import RPCHandler, RPC + +from .uvicorn_threaded import UvicornServer + + +class ApiServer(RPCHandler): + + def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: + super().__init__(rpc, config) + self._server = None + + self.app = FastAPI() + self.configure_app(self.app, self._config) + + self.start_api() + + def cleanup(self) -> None: + """ Cleanup pending module resources """ + if self._server: + self._server.cleanup() + + def send_msg(self, msg: Dict[str, str]) -> None: + pass + + def configure_app(self, app, config): + from .api_v1 import router as api_v1 + app.include_router(api_v1, prefix="/api/v1") + + app.add_middleware( + CORSMiddleware, + allow_origins=config['api_server'].get('CORS_origins', []), + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + def start_api(self): + """ + Start API ... should be run in thread. + """ + uvconfig = uvicorn.Config(self.app, + port=self._config['api_server'].get('listen_port', 8080), + host=self._config['api_server'].get( + 'listen_ip_address', '127.0.0.1'), + access_log=True) + self._server = UvicornServer(uvconfig) + + self._server.run_in_thread() diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 38a4e95fd..2afd39eda 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -34,7 +34,10 @@ class RPCManager: # Enable local rest api server for cmd line control if config.get('api_server', {}).get('enabled', False): logger.info('Enabling rpc.api_server') - from freqtrade.rpc.api_server import ApiServer + # from freqtrade.rpc.api_server import ApiServer + # TODO: Remove the above import + from freqtrade.rpc.api_server2 import ApiServer + self.registered_modules.append(ApiServer(self._rpc, config)) def cleanup(self) -> None: diff --git a/requirements.txt b/requirements.txt index bab13ed03..ad43c1006 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,6 +32,10 @@ flask==1.1.2 flask-jwt-extended==3.25.0 flask-cors==3.0.9 +# API Server +fastapi==0.63.0 +uvicorn==0.13.2 + # Support for colorized terminal output colorama==0.4.4 # Building config files interactively From a862f19f8282e9ee42eebaa0917dcafa320ba08e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Dec 2020 13:08:25 +0100 Subject: [PATCH 088/563] Allow retrieval of rpc and config via dependencies --- freqtrade/rpc/api_server2/api_v1.py | 13 +++++++++--- freqtrade/rpc/api_server2/deps.py | 9 ++++++++ freqtrade/rpc/api_server2/models.py | 29 ++++++++++++++++++++++++++ freqtrade/rpc/api_server2/webserver.py | 14 ++++++++++--- 4 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 freqtrade/rpc/api_server2/deps.py create mode 100644 freqtrade/rpc/api_server2/models.py diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index 1e8bae1d4..2d1491b06 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -1,14 +1,21 @@ from typing import Dict -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from .deps import get_rpc, get_config +from .models import Balances, Ping +# Public API, requires no auth. +router_public = APIRouter() router = APIRouter() -@router.get('/ping') -def _ping() -> Dict[str, str]: +@router_public.get('/ping', response_model=Ping) +def ping(): """simple ping version""" return {"status": "pong"} +@router.get('/balance', response_model=Balances) +def balance(rpc=Depends(get_rpc), config=Depends(get_config)) -> Dict[str, str]: + return rpc._rpc_balance(config['stake_currency'], config.get('fiat_display_currency', ''),) diff --git a/freqtrade/rpc/api_server2/deps.py b/freqtrade/rpc/api_server2/deps.py new file mode 100644 index 000000000..60cc6b8fb --- /dev/null +++ b/freqtrade/rpc/api_server2/deps.py @@ -0,0 +1,9 @@ +from .webserver import ApiServer + + +def get_rpc(): + return ApiServer._rpc + + +def get_config(): + return ApiServer._config diff --git a/freqtrade/rpc/api_server2/models.py b/freqtrade/rpc/api_server2/models.py new file mode 100644 index 000000000..3d1fbf969 --- /dev/null +++ b/freqtrade/rpc/api_server2/models.py @@ -0,0 +1,29 @@ +from typing import List +from pydantic import BaseModel + + +class Ping(BaseModel): + status: str + + class Config: + schema_extra = { + "example": {"status", "pong"} + } + + +class Balance(BaseModel): + currency: str + free: float + balance: float + used: float + est_stake: float + stake: str + + +class Balances(BaseModel): + currencies: List[Balance] + total: float + symbol: str + value: float + stake: str + note: str diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index 3f9baeaff..23cae8b73 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -1,11 +1,9 @@ -import threading from typing import Any, Dict from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import uvicorn -from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.rpc import RPCHandler, RPC from .uvicorn_threaded import UvicornServer @@ -13,10 +11,16 @@ from .uvicorn_threaded import UvicornServer class ApiServer(RPCHandler): + _rpc = None + _config = None + def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: super().__init__(rpc, config) self._server = None + ApiServer._rpc = rpc + ApiServer._config = config + self.app = FastAPI() self.configure_app(self.app, self._config) @@ -30,8 +34,12 @@ class ApiServer(RPCHandler): def send_msg(self, msg: Dict[str, str]) -> None: pass - def configure_app(self, app, config): + def configure_app(self, app: FastAPI, config): + from .api_v1 import router_public as api_v1_public from .api_v1 import router as api_v1 + app.include_router(api_v1_public, prefix="/api/v1") + + # TODO: Include auth dependency! app.include_router(api_v1, prefix="/api/v1") app.add_middleware( From 619b855d5fb203b31b9d4c731b988169f7aef256 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Dec 2020 13:11:01 +0100 Subject: [PATCH 089/563] Add version endpoint --- freqtrade/rpc/api_server2/api_v1.py | 17 ++++++++++++----- freqtrade/rpc/api_server2/models.py | 7 +++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index 2d1491b06..767d72023 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -1,12 +1,14 @@ -from typing import Dict - from fastapi import APIRouter, Depends -from .deps import get_rpc, get_config -from .models import Balances, Ping +from freqtrade import __version__ + +from .deps import get_config, get_rpc +from .models import Balances, Ping, Version + # Public API, requires no auth. router_public = APIRouter() +# Private API, protected by authentication router = APIRouter() @@ -17,5 +19,10 @@ def ping(): @router.get('/balance', response_model=Balances) -def balance(rpc=Depends(get_rpc), config=Depends(get_config)) -> Dict[str, str]: +def balance(rpc=Depends(get_rpc), config=Depends(get_config)): return rpc._rpc_balance(config['stake_currency'], config.get('fiat_display_currency', ''),) + + +@router.get('/version', response_model=Version) +def version(): + return {"version": __version__} diff --git a/freqtrade/rpc/api_server2/models.py b/freqtrade/rpc/api_server2/models.py index 3d1fbf969..7cd628da0 100644 --- a/freqtrade/rpc/api_server2/models.py +++ b/freqtrade/rpc/api_server2/models.py @@ -5,10 +5,9 @@ from pydantic import BaseModel class Ping(BaseModel): status: str - class Config: - schema_extra = { - "example": {"status", "pong"} - } + +class Version(BaseModel): + version: str class Balance(BaseModel): From eac74a9dece972a6fc6eae4d968a701ae6fbb843 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Dec 2020 15:50:19 +0100 Subject: [PATCH 090/563] Implement auth in fastapi --- freqtrade/rpc/api_server2/auth.py | 83 ++++++++++++++++++++++++++ freqtrade/rpc/api_server2/models.py | 8 +++ freqtrade/rpc/api_server2/webserver.py | 24 ++++---- requirements.txt | 1 + 4 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 freqtrade/rpc/api_server2/auth.py diff --git a/freqtrade/rpc/api_server2/auth.py b/freqtrade/rpc/api_server2/auth.py new file mode 100644 index 000000000..3155e7754 --- /dev/null +++ b/freqtrade/rpc/api_server2/auth.py @@ -0,0 +1,83 @@ +from freqtrade.rpc.api_server2.models import AccessAndRefreshToken, AccessToken +import secrets +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.security.http import HTTPBasic, HTTPBasicCredentials +from fastapi.security.utils import get_authorization_scheme_param +from fastapi_jwt_auth import AuthJWT +from pydantic import BaseModel + +from .deps import get_config + + +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +router_login = APIRouter() + + +class Settings(BaseModel): + # TODO: should be set as config['api_server'].get('jwt_secret_key', 'super-secret') + authjwt_secret_key: str = "secret" + + +@AuthJWT.load_config +def get_jwt_config(): + return Settings() + + +def verify_auth(config, username: str, password: str): + return (secrets.compare_digest(username, config['api_server'].get('username')) and + secrets.compare_digest(password, config['api_server'].get('password'))) + + +class HTTPBasicOrJWTToken(HTTPBasic): + description = "Token Or Pass auth" + + async def __call__(self, request: Request, config=Depends(get_config) # type: ignore + ) -> Optional[str]: + header_authorization: str = request.headers.get("Authorization") + header_scheme, header_param = get_authorization_scheme_param(header_authorization) + if header_scheme.lower() == 'bearer': + AuthJWT.jwt_required() + elif header_scheme.lower() == 'basic': + credentials: Optional[HTTPBasicCredentials] = await HTTPBasic()(request) + if credentials and verify_auth(config, credentials.username, credentials.password): + return credentials.username + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Basic"}, + ) + + +@router_login.post('/token/login', response_model=AccessAndRefreshToken) +def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()), config=Depends(get_config)): + + print(form_data) + Authorize = AuthJWT() + + if verify_auth(config, form_data.username, form_data.password): + token_data = form_data.username + access_token = Authorize.create_access_token(subject=token_data) + refresh_token = Authorize.create_refresh_token(subject=token_data) + return { + "access_token": access_token, + "refresh_token": refresh_token, + } + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Basic"}, + ) + + +@router_login.post('/token/refresh', response_model=AccessToken) +def token_refresh(Authorize: AuthJWT = Depends()): + Authorize.jwt_refresh_token_required() + + access_token = Authorize.create_access_token(subject=Authorize.get_jwt_subject()) + return {'access_token': access_token} diff --git a/freqtrade/rpc/api_server2/models.py b/freqtrade/rpc/api_server2/models.py index 7cd628da0..c9ddb0d6f 100644 --- a/freqtrade/rpc/api_server2/models.py +++ b/freqtrade/rpc/api_server2/models.py @@ -6,6 +6,14 @@ class Ping(BaseModel): status: str +class AccessToken(BaseModel): + access_token: str + + +class AccessAndRefreshToken(AccessToken): + refresh_token: str + + class Version(BaseModel): version: str diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index 23cae8b73..84f6fc222 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -1,18 +1,17 @@ -from typing import Any, Dict - -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware +from typing import Any, Dict, Optional import uvicorn +from fastapi import Depends, FastAPI +from fastapi.middleware.cors import CORSMiddleware -from freqtrade.rpc.rpc import RPCHandler, RPC +from freqtrade.rpc.rpc import RPC, RPCHandler from .uvicorn_threaded import UvicornServer class ApiServer(RPCHandler): - _rpc = None - _config = None + _rpc: Optional[RPC] = None + _config: Dict[str, Any] = {} def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: super().__init__(rpc, config) @@ -21,7 +20,7 @@ class ApiServer(RPCHandler): ApiServer._rpc = rpc ApiServer._config = config - self.app = FastAPI() + self.app = FastAPI(title="Freqtrade API") self.configure_app(self.app, self._config) self.start_api() @@ -35,12 +34,15 @@ class ApiServer(RPCHandler): pass def configure_app(self, app: FastAPI, config): - from .api_v1 import router_public as api_v1_public from .api_v1 import router as api_v1 + from .api_v1 import router_public as api_v1_public + from .auth import router_login, HTTPBasicOrJWTToken app.include_router(api_v1_public, prefix="/api/v1") - # TODO: Include auth dependency! - app.include_router(api_v1, prefix="/api/v1") + app.include_router(api_v1, prefix="/api/v1", + dependencies=[Depends(HTTPBasicOrJWTToken())] + ) + app.include_router(router_login, prefix="/api/v1") app.add_middleware( CORSMiddleware, diff --git a/requirements.txt b/requirements.txt index ad43c1006..7cda9e48c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,6 +35,7 @@ flask-cors==3.0.9 # API Server fastapi==0.63.0 uvicorn==0.13.2 +fastapi_jwt_auth==0.5.0 # Support for colorized terminal output colorama==0.4.4 From 6594278509feb76b878fa07cbb77d52d5cb870a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Dec 2020 15:57:05 +0100 Subject: [PATCH 091/563] Reorder endpoints --- freqtrade/rpc/api_server2/api_v1.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index 767d72023..c2e7a9172 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -18,11 +18,13 @@ def ping(): return {"status": "pong"} +@router.get('/version', response_model=Version) +def version(): + return {"version": __version__} + + @router.get('/balance', response_model=Balances) def balance(rpc=Depends(get_rpc), config=Depends(get_config)): return rpc._rpc_balance(config['stake_currency'], config.get('fiat_display_currency', ''),) -@router.get('/version', response_model=Version) -def version(): - return {"version": __version__} From 86d07008848086607aaa5183127760a069d6091b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Dec 2020 20:07:12 +0100 Subject: [PATCH 092/563] Move models to apimodels --- .../rpc/api_server2/{auth.py => api_auth.py} | 6 +++--- .../api_server2/{models.py => api_models.py} | 5 +++++ freqtrade/rpc/api_server2/api_v1.py | 17 +++++++++++++---- freqtrade/rpc/api_server2/uvicorn_threaded.py | 3 ++- freqtrade/rpc/api_server2/webserver.py | 3 ++- 5 files changed, 25 insertions(+), 9 deletions(-) rename freqtrade/rpc/api_server2/{auth.py => api_auth.py} (94%) rename freqtrade/rpc/api_server2/{models.py => api_models.py} (91%) diff --git a/freqtrade/rpc/api_server2/auth.py b/freqtrade/rpc/api_server2/api_auth.py similarity index 94% rename from freqtrade/rpc/api_server2/auth.py rename to freqtrade/rpc/api_server2/api_auth.py index 3155e7754..d0c975480 100644 --- a/freqtrade/rpc/api_server2/auth.py +++ b/freqtrade/rpc/api_server2/api_auth.py @@ -1,4 +1,3 @@ -from freqtrade.rpc.api_server2.models import AccessAndRefreshToken, AccessToken import secrets from typing import Optional @@ -8,6 +7,8 @@ from fastapi.security.utils import get_authorization_scheme_param from fastapi_jwt_auth import AuthJWT from pydantic import BaseModel +from freqtrade.rpc.api_server2.api_models import AccessAndRefreshToken, AccessToken + from .deps import get_config @@ -41,7 +42,7 @@ class HTTPBasicOrJWTToken(HTTPBasic): header_authorization: str = request.headers.get("Authorization") header_scheme, header_param = get_authorization_scheme_param(header_authorization) if header_scheme.lower() == 'bearer': - AuthJWT.jwt_required() + AuthJWT(request).jwt_required() elif header_scheme.lower() == 'basic': credentials: Optional[HTTPBasicCredentials] = await HTTPBasic()(request) if credentials and verify_auth(config, credentials.username, credentials.password): @@ -49,7 +50,6 @@ class HTTPBasicOrJWTToken(HTTPBasic): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", - headers={"WWW-Authenticate": "Basic"}, ) diff --git a/freqtrade/rpc/api_server2/models.py b/freqtrade/rpc/api_server2/api_models.py similarity index 91% rename from freqtrade/rpc/api_server2/models.py rename to freqtrade/rpc/api_server2/api_models.py index c9ddb0d6f..0646ce3e6 100644 --- a/freqtrade/rpc/api_server2/models.py +++ b/freqtrade/rpc/api_server2/api_models.py @@ -1,4 +1,5 @@ from typing import List + from pydantic import BaseModel @@ -18,6 +19,10 @@ class Version(BaseModel): version: str +class StatusMsg(BaseModel): + status: str + + class Balance(BaseModel): currency: str free: float diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index c2e7a9172..b3e0a7a4f 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -1,9 +1,10 @@ +from freqtrade.rpc import RPC from fastapi import APIRouter, Depends from freqtrade import __version__ +from .api_models import Balances, Ping, StatusMsg, Version from .deps import get_config, get_rpc -from .models import Balances, Ping, Version # Public API, requires no auth. @@ -18,13 +19,21 @@ def ping(): return {"status": "pong"} -@router.get('/version', response_model=Version) +@router.get('/version', response_model=Version, tags=['info']) def version(): return {"version": __version__} -@router.get('/balance', response_model=Balances) -def balance(rpc=Depends(get_rpc), config=Depends(get_config)): +@router.get('/balance', response_model=Balances, tags=['info']) +def balance(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): return rpc._rpc_balance(config['stake_currency'], config.get('fiat_display_currency', ''),) +@router.post('/start', response_model=StatusMsg, tags=['botcontrol']) +def start(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_start() + + +@router.post('/stop', response_model=StatusMsg, tags=['botcontrol']) +def stop(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_stop() diff --git a/freqtrade/rpc/api_server2/uvicorn_threaded.py b/freqtrade/rpc/api_server2/uvicorn_threaded.py index ba9263620..7c8804fd3 100644 --- a/freqtrade/rpc/api_server2/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server2/uvicorn_threaded.py @@ -1,6 +1,7 @@ import contextlib -import time import threading +import time + import uvicorn diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index 84f6fc222..5f2e1d6fe 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -1,4 +1,5 @@ from typing import Any, Dict, Optional + import uvicorn from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -36,7 +37,7 @@ class ApiServer(RPCHandler): def configure_app(self, app: FastAPI, config): from .api_v1 import router as api_v1 from .api_v1 import router_public as api_v1_public - from .auth import router_login, HTTPBasicOrJWTToken + from .api_auth import HTTPBasicOrJWTToken, router_login app.include_router(api_v1_public, prefix="/api/v1") app.include_router(api_v1, prefix="/api/v1", From 5e4c4cae06e27f0ad110d73f89e8f3fde4e4338a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Dec 2020 08:48:15 +0100 Subject: [PATCH 093/563] Fix auth providers --- freqtrade/rpc/api_server2/api_auth.py | 102 +++++++++++++++---------- freqtrade/rpc/api_server2/webserver.py | 6 +- requirements.txt | 2 +- 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_auth.py b/freqtrade/rpc/api_server2/api_auth.py index d0c975480..cf0168576 100644 --- a/freqtrade/rpc/api_server2/api_auth.py +++ b/freqtrade/rpc/api_server2/api_auth.py @@ -1,12 +1,11 @@ +from datetime import datetime, timedelta import secrets -from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.security.http import HTTPBasic, HTTPBasicCredentials -from fastapi.security.utils import get_authorization_scheme_param -from fastapi_jwt_auth import AuthJWT from pydantic import BaseModel - +import jwt +from fastapi.security import OAuth2PasswordBearer from freqtrade.rpc.api_server2.api_models import AccessAndRefreshToken, AccessToken from .deps import get_config @@ -19,50 +18,72 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30 router_login = APIRouter() -class Settings(BaseModel): - # TODO: should be set as config['api_server'].get('jwt_secret_key', 'super-secret') - authjwt_secret_key: str = "secret" - - -@AuthJWT.load_config -def get_jwt_config(): - return Settings() - - def verify_auth(config, username: str, password: str): + """Verify username/password""" return (secrets.compare_digest(username, config['api_server'].get('username')) and secrets.compare_digest(password, config['api_server'].get('password'))) -class HTTPBasicOrJWTToken(HTTPBasic): - description = "Token Or Pass auth" +httpbasic = HTTPBasic(auto_error=False) +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) - async def __call__(self, request: Request, config=Depends(get_config) # type: ignore - ) -> Optional[str]: - header_authorization: str = request.headers.get("Authorization") - header_scheme, header_param = get_authorization_scheme_param(header_authorization) - if header_scheme.lower() == 'bearer': - AuthJWT(request).jwt_required() - elif header_scheme.lower() == 'basic': - credentials: Optional[HTTPBasicCredentials] = await HTTPBasic()(request) - if credentials and verify_auth(config, credentials.username, credentials.password): - return credentials.username - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - ) + +def get_user_from_token(token, token_type: str = "access"): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + if payload.get("type") != token_type: + raise credentials_exception + + except jwt.PyJWTError: + raise credentials_exception + return username + + +def create_token(data: dict, token_type: str = "access") -> str: + to_encode = data.copy() + if token_type == "access": + expire = datetime.utcnow() + timedelta(minutes=15) + elif token_type == "refresh": + expire = datetime.utcnow() + timedelta(days=30) + else: + raise ValueError() + to_encode.update({ + "exp": expire, + "iat": datetime.utcnow(), + "type": token_type, + }) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def http_basic_or_jwt_token(form_data: HTTPBasicCredentials = Depends(httpbasic), + token: str = Depends(oauth2_scheme), config=Depends(get_config)): + if token: + return get_user_from_token(token) + elif form_data and verify_auth(config, form_data.username, form_data.password): + return form_data.username + + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + ) @router_login.post('/token/login', response_model=AccessAndRefreshToken) def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()), config=Depends(get_config)): - print(form_data) - Authorize = AuthJWT() - if verify_auth(config, form_data.username, form_data.password): - token_data = form_data.username - access_token = Authorize.create_access_token(subject=token_data) - refresh_token = Authorize.create_refresh_token(subject=token_data) + token_data = {'sub': form_data.username} + access_token = create_token(token_data) + refresh_token = create_token(token_data, token_type="refresh") return { "access_token": access_token, "refresh_token": refresh_token, @@ -76,8 +97,9 @@ def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()), config=D @router_login.post('/token/refresh', response_model=AccessToken) -def token_refresh(Authorize: AuthJWT = Depends()): - Authorize.jwt_refresh_token_required() - - access_token = Authorize.create_access_token(subject=Authorize.get_jwt_subject()) +def token_refresh(token: str = Depends(oauth2_scheme)): + # Refresh token + u = get_user_from_token(token, 'refresh') + token_data = {'sub': u} + access_token = create_token(token_data, token_type="access") return {'access_token': access_token} diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index 5f2e1d6fe..755b43127 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -37,13 +37,13 @@ class ApiServer(RPCHandler): def configure_app(self, app: FastAPI, config): from .api_v1 import router as api_v1 from .api_v1 import router_public as api_v1_public - from .api_auth import HTTPBasicOrJWTToken, router_login + from .api_auth import http_basic_or_jwt_token, router_login app.include_router(api_v1_public, prefix="/api/v1") app.include_router(api_v1, prefix="/api/v1", - dependencies=[Depends(HTTPBasicOrJWTToken())] + dependencies=[Depends(http_basic_or_jwt_token)], ) - app.include_router(router_login, prefix="/api/v1") + app.include_router(router_login, prefix="/api/v1", tags=["auth"]) app.add_middleware( CORSMiddleware, diff --git a/requirements.txt b/requirements.txt index 7cda9e48c..4b439079b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ flask-cors==3.0.9 # API Server fastapi==0.63.0 uvicorn==0.13.2 -fastapi_jwt_auth==0.5.0 +pyjwt==1.7.1 # Support for colorized terminal output colorama==0.4.4 From 4b86700a0fd0dac1ce95acf37ed59b831b7812b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Dec 2020 15:54:22 +0100 Subject: [PATCH 094/563] Implement more endpoints --- freqtrade/rpc/api_server2/api_auth.py | 7 ++++--- freqtrade/rpc/api_server2/api_models.py | 6 ++++++ freqtrade/rpc/api_server2/api_v1.py | 24 ++++++++++++++++++++++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_auth.py b/freqtrade/rpc/api_server2/api_auth.py index cf0168576..cb19d7637 100644 --- a/freqtrade/rpc/api_server2/api_auth.py +++ b/freqtrade/rpc/api_server2/api_auth.py @@ -1,11 +1,12 @@ -from datetime import datetime, timedelta import secrets +from datetime import datetime, timedelta +import jwt from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.security import OAuth2PasswordBearer from fastapi.security.http import HTTPBasic, HTTPBasicCredentials from pydantic import BaseModel -import jwt -from fastapi.security import OAuth2PasswordBearer + from freqtrade.rpc.api_server2.api_models import AccessAndRefreshToken, AccessToken from .deps import get_config diff --git a/freqtrade/rpc/api_server2/api_models.py b/freqtrade/rpc/api_server2/api_models.py index 0646ce3e6..92e439b21 100644 --- a/freqtrade/rpc/api_server2/api_models.py +++ b/freqtrade/rpc/api_server2/api_models.py @@ -39,3 +39,9 @@ class Balances(BaseModel): value: float stake: str note: str + + +class Count(BaseModel): + current: int + max: int + total_stake: float diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index b3e0a7a4f..e05db3ace 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -1,9 +1,9 @@ -from freqtrade.rpc import RPC from fastapi import APIRouter, Depends from freqtrade import __version__ +from freqtrade.rpc import RPC -from .api_models import Balances, Ping, StatusMsg, Version +from .api_models import Balances, Count, Ping, StatusMsg, Version from .deps import get_config, get_rpc @@ -29,6 +29,16 @@ def balance(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): return rpc._rpc_balance(config['stake_currency'], config.get('fiat_display_currency', ''),) +@router.get('/count', response_model=Count, tags=['info']) +def count(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_count() + + +@router.get('/show_config', tags=['info']) +def show_config(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): + return RPC._rpc_show_config(config, rpc._freqtrade.state) + + @router.post('/start', response_model=StatusMsg, tags=['botcontrol']) def start(rpc: RPC = Depends(get_rpc)): return rpc._rpc_start() @@ -37,3 +47,13 @@ def start(rpc: RPC = Depends(get_rpc)): @router.post('/stop', response_model=StatusMsg, tags=['botcontrol']) def stop(rpc: RPC = Depends(get_rpc)): return rpc._rpc_stop() + + +@router.post('/stopbuy', response_model=StatusMsg, tags=['botcontrol']) +def stop_buy(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_stopbuy() + + +@router.post('/reload_config', response_model=StatusMsg, tags=['botcontrol']) +def reload_config(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_reload_config() From f37ea4ba248923261648e2e509cc6e09f403365d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Dec 2020 16:33:13 +0100 Subject: [PATCH 095/563] Fix some initial tests towards fastAPI --- tests/rpc/test_rpc_apiserver.py | 76 +++++++++++++++++---------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8da4ebfe7..7e777b732 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -7,6 +7,7 @@ from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock import pytest +from fastapi.testclient import TestClient from flask import Flask from requests.auth import _basic_auth_str @@ -14,7 +15,8 @@ from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC -from freqtrade.rpc.api_server import BASE_URI, ApiServer +from freqtrade.rpc.api_server import BASE_URI # , ApiServer +from freqtrade.rpc.api_server2 import ApiServer from freqtrade.state import RunMode, State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -38,18 +40,19 @@ def botclient(default_conf, mocker): ftbot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(ftbot) - mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) + mocker.patch('freqtrade.rpc.api_server2.ApiServer.start_api', MagicMock()) apiserver = ApiServer(rpc, default_conf) - yield ftbot, apiserver.app.test_client() + yield ftbot, TestClient(apiserver.app) # Cleanup ... ? def client_post(client, url, data={}): return client.post(url, - content_type="application/json", data=data, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), - 'Origin': 'http://example.com'}) + 'Origin': 'http://example.com', + 'content-type': 'application/json' + }) def client_get(client, url): @@ -66,10 +69,10 @@ def client_delete(client, url): def assert_response(response, expected_code=200, needs_cors=True): assert response.status_code == expected_code - assert response.content_type == "application/json" + assert response.headers.get('content-type') == "application/json" if needs_cors: - assert ('Access-Control-Allow-Credentials', 'true') in response.headers._list - assert ('Access-Control-Allow-Origin', 'http://example.com') in response.headers._list + assert ('access-control-allow-credentials', 'true') in response.headers.items() + assert ('access-control-allow-origin', 'http://example.com') in response.headers.items() def test_api_not_found(botclient): @@ -77,7 +80,7 @@ def test_api_not_found(botclient): rc = client_post(client, f"{BASE_URI}/invalid_url") assert_response(rc, 404) - assert rc.json == {"status": "error", + assert rc.json() == {"status": "error", "reason": f"There's no API call for http://localhost{BASE_URI}/invalid_url.", "code": 404 } @@ -87,45 +90,44 @@ def test_api_unauthorized(botclient): ftbot, client = botclient rc = client.get(f"{BASE_URI}/ping") assert_response(rc, needs_cors=False) - assert rc.json == {'status': 'pong'} + assert rc.json() == {'status': 'pong'} # Don't send user/pass information rc = client.get(f"{BASE_URI}/version") assert_response(rc, 401, needs_cors=False) - assert rc.json == {'error': 'Unauthorized'} + assert rc.json() == {'error': 'Unauthorized'} # Change only username ftbot.config['api_server']['username'] = 'Ftrader' rc = client_get(client, f"{BASE_URI}/version") assert_response(rc, 401) - assert rc.json == {'error': 'Unauthorized'} + assert rc.json() == {'error': 'Unauthorized'} # Change only password ftbot.config['api_server']['username'] = _TEST_USER ftbot.config['api_server']['password'] = 'WrongPassword' rc = client_get(client, f"{BASE_URI}/version") assert_response(rc, 401) - assert rc.json == {'error': 'Unauthorized'} + assert rc.json() == {'error': 'Unauthorized'} ftbot.config['api_server']['username'] = 'Ftrader' ftbot.config['api_server']['password'] = 'WrongPassword' rc = client_get(client, f"{BASE_URI}/version") assert_response(rc, 401) - assert rc.json == {'error': 'Unauthorized'} + assert rc.json() == {'error': 'Unauthorized'} def test_api_token_login(botclient): ftbot, client = botclient rc = client_post(client, f"{BASE_URI}/token/login") assert_response(rc) - assert 'access_token' in rc.json - assert 'refresh_token' in rc.json + assert 'access_token' in rc.json() + assert 'refresh_token' in rc.json() # test Authentication is working with JWT tokens too rc = client.get(f"{BASE_URI}/count", - content_type="application/json", - headers={'Authorization': f'Bearer {rc.json["access_token"]}', + headers={'Authorization': f'Bearer {rc.json()["access_token"]}', 'Origin': 'http://example.com'}) assert_response(rc) @@ -268,7 +270,7 @@ def test_api_reloadconf(botclient): rc = client_post(client, f"{BASE_URI}/reload_config") assert_response(rc) - assert rc.json == {'status': 'Reloading config ...'} + assert rc.json() == {'status': 'Reloading config ...'} assert ftbot.state == State.RELOAD_CONFIG @@ -278,7 +280,7 @@ def test_api_stopbuy(botclient): rc = client_post(client, f"{BASE_URI}/stopbuy") assert_response(rc) - assert rc.json == {'status': 'No more buy will occur from now. Run /reload_config to reset.'} + assert rc.json() == {'status': 'No more buy will occur from now. Run /reload_config to reset.'} assert ftbot.config['max_open_trades'] == 0 @@ -293,9 +295,9 @@ def test_api_balance(botclient, mocker, rpc_balance): rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) - assert "currencies" in rc.json - assert len(rc.json["currencies"]) == 5 - assert rc.json['currencies'][0] == { + assert "currencies" in rc.json() + assert len(rc.json()["currencies"]) == 5 + assert rc.json()['currencies'][0] == { 'currency': 'BTC', 'free': 12.0, 'balance': 12.0, @@ -318,15 +320,15 @@ def test_api_count(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) - assert rc.json["current"] == 0 - assert rc.json["max"] == 1.0 + assert rc.json()["current"] == 0 + assert rc.json()["max"] == 1.0 # Create some test data ftbot.enter_positions() rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) - assert rc.json["current"] == 1.0 - assert rc.json["max"] == 1.0 + assert rc.json()["current"] == 1.0 + assert rc.json()["max"] == 1.0 def test_api_locks(botclient): @@ -359,15 +361,15 @@ def test_api_show_config(botclient, mocker): rc = client_get(client, f"{BASE_URI}/show_config") assert_response(rc) - assert 'dry_run' in rc.json - assert rc.json['exchange'] == 'bittrex' - assert rc.json['timeframe'] == '5m' - assert rc.json['timeframe_ms'] == 300000 - assert rc.json['timeframe_min'] == 5 - assert rc.json['state'] == 'running' - assert not rc.json['trailing_stop'] - assert 'bid_strategy' in rc.json - assert 'ask_strategy' in rc.json + assert 'dry_run' in rc.json() + assert rc.json()['exchange'] == 'bittrex' + assert rc.json()['timeframe'] == '5m' + assert rc.json()['timeframe_ms'] == 300000 + assert rc.json()['timeframe_min'] == 5 + assert rc.json()['state'] == 'running' + assert not rc.json()['trailing_stop'] + assert 'bid_strategy' in rc.json() + assert 'ask_strategy' in rc.json() def test_api_daily(botclient, mocker, ticker, fee, markets): @@ -722,7 +724,7 @@ def test_api_version(botclient): rc = client_get(client, f"{BASE_URI}/version") assert_response(rc) - assert rc.json == {"version": __version__} + assert rc.json() == {"version": __version__} def test_api_blacklist(botclient, mocker): From a18d66e10814e4c0f53618a07c3c5f1a0df17c31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Dec 2020 16:43:15 +0100 Subject: [PATCH 096/563] Add more endpoints to fastapi --- freqtrade/rpc/api_server2/api_models.py | 47 ++++++++++++++- freqtrade/rpc/api_server2/api_v1.py | 21 ++++++- tests/rpc/test_rpc_apiserver.py | 80 ++++++++++++------------- 3 files changed, 106 insertions(+), 42 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_models.py b/freqtrade/rpc/api_server2/api_models.py index 92e439b21..293b1e97f 100644 --- a/freqtrade/rpc/api_server2/api_models.py +++ b/freqtrade/rpc/api_server2/api_models.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Dict, List, Union from pydantic import BaseModel @@ -45,3 +45,48 @@ class Count(BaseModel): current: int max: int total_stake: float + + +class PerformanceEntry(BaseModel): + pair: str + profit: float + count: int + + +class Profit(BaseModel): + profit_closed_coin: float + profit_closed_percent: float + profit_closed_percent_mean: float + profit_closed_ratio_mean: float + profit_closed_percent_sum: float + profit_closed_ratio_sum: float + profit_closed_fiat: float + profit_all_coin: float + profit_all_percent: float + profit_all_percent_mean: float + profit_all_ratio_mean: float + profit_all_percent_sum: float + profit_all_ratio_sum: float + profit_all_fiat: float + trade_count: int + closed_trade_count: int + first_trade_date: str + first_trade_timestamp: int + latest_trade_date: str + latest_trade_timestamp: int + avg_duration: str + best_pair: str + best_rate: float + winning_trades: int + losing_trades: int + + +class SellReason(BaseModel): + wins: int + losses: int + draws: int + + +class Stats(BaseModel): + sell_reasons: Dict[str, SellReason] + durations: Dict[str, Union[str, float]] diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index e05db3ace..78a7ebbf8 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -1,9 +1,11 @@ +from typing import List + from fastapi import APIRouter, Depends from freqtrade import __version__ from freqtrade.rpc import RPC -from .api_models import Balances, Count, Ping, StatusMsg, Version +from .api_models import Balances, Count, PerformanceEntry, Ping, Profit, Stats, StatusMsg, Version from .deps import get_config, get_rpc @@ -34,6 +36,23 @@ def count(rpc: RPC = Depends(get_rpc)): return rpc._rpc_count() +@router.get('/performance', response_model=List[PerformanceEntry], tags=['info']) +def performance(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_performance() + + +@router.get('/profit', response_model=Profit, tags=['info']) +def profit(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): + return rpc._rpc_trade_statistics(config['stake_currency'], + config.get('fiat_display_currency') + ) + + +@router.get('/stats', response_model=Stats, tags=['info']) +def stats(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_stats() + + @router.get('/show_config', tags=['info']) def show_config(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): return RPC._rpc_show_config(config, rpc._freqtrade.state) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7e777b732..b700714a8 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -512,7 +512,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc, 200) - assert rc.json['trade_count'] == 0 + assert rc.json()['trade_count'] == 0 ftbot.enter_positions() trade = Trade.query.first() @@ -522,9 +522,9 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc, 200) # One open trade - assert rc.json['trade_count'] == 1 - assert rc.json['best_pair'] == '' - assert rc.json['best_rate'] == 0 + assert rc.json()['trade_count'] == 1 + assert rc.json()['best_pair'] == '' + assert rc.json()['best_rate'] == 0 trade = Trade.query.first() trade.update(limit_sell_order) @@ -534,32 +534,32 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc) - assert rc.json == {'avg_duration': '0:00:00', - 'best_pair': 'ETH/BTC', - 'best_rate': 6.2, - 'first_trade_date': 'just now', - 'first_trade_timestamp': ANY, - 'latest_trade_date': 'just now', - 'latest_trade_timestamp': ANY, - 'profit_all_coin': 6.217e-05, - 'profit_all_fiat': 0.76748865, - 'profit_all_percent': 6.2, - 'profit_all_percent_mean': 6.2, - 'profit_all_ratio_mean': 0.06201058, - 'profit_all_percent_sum': 6.2, - 'profit_all_ratio_sum': 0.06201058, - 'profit_closed_coin': 6.217e-05, - 'profit_closed_fiat': 0.76748865, - 'profit_closed_percent': 6.2, - 'profit_closed_ratio_mean': 0.06201058, - 'profit_closed_percent_mean': 6.2, - 'profit_closed_ratio_sum': 0.06201058, - 'profit_closed_percent_sum': 6.2, - 'trade_count': 1, - 'closed_trade_count': 1, - 'winning_trades': 1, - 'losing_trades': 0, - } + assert rc.json() == {'avg_duration': '0:00:00', + 'best_pair': 'ETH/BTC', + 'best_rate': 6.2, + 'first_trade_date': 'just now', + 'first_trade_timestamp': ANY, + 'latest_trade_date': 'just now', + 'latest_trade_timestamp': ANY, + 'profit_all_coin': 6.217e-05, + 'profit_all_fiat': 0.76748865, + 'profit_all_percent': 6.2, + 'profit_all_percent_mean': 6.2, + 'profit_all_ratio_mean': 0.06201058, + 'profit_all_percent_sum': 6.2, + 'profit_all_ratio_sum': 0.06201058, + 'profit_closed_coin': 6.217e-05, + 'profit_closed_fiat': 0.76748865, + 'profit_closed_percent': 6.2, + 'profit_closed_ratio_mean': 0.06201058, + 'profit_closed_percent_mean': 6.2, + 'profit_closed_ratio_sum': 0.06201058, + 'profit_closed_percent_sum': 6.2, + 'trade_count': 1, + 'closed_trade_count': 1, + 'winning_trades': 1, + 'losing_trades': 0, + } @pytest.mark.usefixtures("init_persistence") @@ -576,19 +576,19 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,): rc = client_get(client, f"{BASE_URI}/stats") assert_response(rc, 200) - assert 'durations' in rc.json - assert 'sell_reasons' in rc.json + assert 'durations' in rc.json() + assert 'sell_reasons' in rc.json() create_mock_trades(fee) rc = client_get(client, f"{BASE_URI}/stats") assert_response(rc, 200) - assert 'durations' in rc.json - assert 'sell_reasons' in rc.json + assert 'durations' in rc.json() + assert 'sell_reasons' in rc.json() - assert 'wins' in rc.json['durations'] - assert 'losses' in rc.json['durations'] - assert 'draws' in rc.json['durations'] + assert 'wins' in rc.json()['durations'] + assert 'losses' in rc.json()['durations'] + assert 'draws' in rc.json()['durations'] def test_api_performance(botclient, mocker, ticker, fee): @@ -629,9 +629,9 @@ def test_api_performance(botclient, mocker, ticker, fee): rc = client_get(client, f"{BASE_URI}/performance") assert_response(rc) - assert len(rc.json) == 2 - assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, - {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}] + assert len(rc.json()) == 2 + assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, + {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}] def test_api_status(botclient, mocker, ticker, fee, markets): From 73a29e6d7480857fb2ba2af81df8ac95c1bd0b45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Dec 2020 17:00:30 +0100 Subject: [PATCH 097/563] Improve tests, implement more fastapi methods --- freqtrade/rpc/api_server2/api_models.py | 11 +++++++++- freqtrade/rpc/api_server2/api_v1.py | 27 ++++++++++++++++++++++++- freqtrade/rpc/api_server2/webserver.py | 19 +++++++++++++++-- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_models.py b/freqtrade/rpc/api_server2/api_models.py index 293b1e97f..aa9dfcc33 100644 --- a/freqtrade/rpc/api_server2/api_models.py +++ b/freqtrade/rpc/api_server2/api_models.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union from pydantic import BaseModel @@ -90,3 +90,12 @@ class SellReason(BaseModel): class Stats(BaseModel): sell_reasons: Dict[str, SellReason] durations: Dict[str, Union[str, float]] + + +class ForceBuyPayload(BaseModel): + pair: str + price: Optional[float] + + +class ForceSellPayload(BaseModel): + tradeid: str diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index 78a7ebbf8..961eb2c78 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -4,8 +4,10 @@ from fastapi import APIRouter, Depends from freqtrade import __version__ from freqtrade.rpc import RPC +from freqtrade.rpc.rpc import RPCException -from .api_models import Balances, Count, PerformanceEntry, Ping, Profit, Stats, StatusMsg, Version +from .api_models import (Balances, Count, ForceBuyPayload, ForceSellPayload, PerformanceEntry, Ping, Profit, Stats, + StatusMsg, Version) from .deps import get_config, get_rpc @@ -53,10 +55,33 @@ def stats(rpc: RPC = Depends(get_rpc)): return rpc._rpc_stats() +# TODO: Missing response model +@router.get('/status', tags=['info']) +def status(rpc: RPC = Depends(get_rpc)): + try: + return rpc._rpc_trade_status() + except RPCException: + return [] + + @router.get('/show_config', tags=['info']) def show_config(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): return RPC._rpc_show_config(config, rpc._freqtrade.state) +@router.post('/forcebuy', tags=['trading']) +def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): + trade = rpc._rpc_forcebuy(payload.pair, payload.price) + + if trade: + return trade.to_json() + else: + return {"status": f"Error buying pair {payload.pair}."} + + +@router.post('/forcesell', tags=['trading']) +def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_forcesell(payload.tradeid) + @router.post('/start', response_model=StatusMsg, tags=['botcontrol']) def start(rpc: RPC = Depends(get_rpc)): diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index 755b43127..45e695151 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -1,14 +1,20 @@ +import logging from typing import Any, Dict, Optional +from starlette.responses import JSONResponse import uvicorn from fastapi import Depends, FastAPI +from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware -from freqtrade.rpc.rpc import RPC, RPCHandler +from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler from .uvicorn_threaded import UvicornServer +logger = logging.getLogger(__name__) + + class ApiServer(RPCHandler): _rpc: Optional[RPC] = None @@ -34,10 +40,17 @@ class ApiServer(RPCHandler): def send_msg(self, msg: Dict[str, str]) -> None: pass + def handle_rpc_exception(self, request, exc): + logger.exception(f"API Error calling: {exc}") + return JSONResponse( + status_code=502, + content={'error': f"Error querying {request.url.path}: {exc.message}"} + ) + def configure_app(self, app: FastAPI, config): + from .api_auth import http_basic_or_jwt_token, router_login from .api_v1 import router as api_v1 from .api_v1 import router_public as api_v1_public - from .api_auth import http_basic_or_jwt_token, router_login app.include_router(api_v1_public, prefix="/api/v1") app.include_router(api_v1, prefix="/api/v1", @@ -53,6 +66,8 @@ class ApiServer(RPCHandler): allow_headers=["*"], ) + app.add_exception_handler(RPCException, self.handle_rpc_exception) + def start_api(self): """ Start API ... should be run in thread. From 9ee1d8835595cd688efd7e4db7eca4dd452d3249 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Dec 2020 17:33:27 +0100 Subject: [PATCH 098/563] Implement more endpoints --- freqtrade/rpc/api_server2/api_models.py | 65 ++++- freqtrade/rpc/api_server2/api_v1.py | 59 +++- tests/rpc/test_rpc_apiserver.py | 346 ++++++++++++------------ 3 files changed, 295 insertions(+), 175 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_models.py b/freqtrade/rpc/api_server2/api_models.py index aa9dfcc33..a30973845 100644 --- a/freqtrade/rpc/api_server2/api_models.py +++ b/freqtrade/rpc/api_server2/api_models.py @@ -1,5 +1,5 @@ from typing import Dict, List, Optional, Union - +from datetime import date from pydantic import BaseModel @@ -23,6 +23,10 @@ class StatusMsg(BaseModel): status: str +class ResultMsg(BaseModel): + result: str + + class Balance(BaseModel): currency: str free: float @@ -92,6 +96,39 @@ class Stats(BaseModel): durations: Dict[str, Union[str, float]] +class DailyRecord(BaseModel): + date: date + abs_profit: float + fiat_value: float + trade_count: int + + +class Daily(BaseModel): + data: List[DailyRecord] + fiat_display_currency: str + stake_currency: str + + +class LockModel(BaseModel): + active: bool + lock_end_time: str + lock_end_timestamp: int + lock_time: str + lock_timestamp: int + pair: str + reason: str + + +class Locks(BaseModel): + lock_count: int + locks: List[LockModel] + + +class Logs(BaseModel): + log_count: int + logs: List[List] + + class ForceBuyPayload(BaseModel): pair: str price: Optional[float] @@ -99,3 +136,29 @@ class ForceBuyPayload(BaseModel): class ForceSellPayload(BaseModel): tradeid: str + + +class BlacklistPayload(BaseModel): + blacklist: List[str] + + +class BlacklistResponse(BaseModel): + blacklist: List[str] + blacklist_expanded: List[str] + errors: Dict + length: int + method: List[str] + + +class WhitelistResponse(BaseModel): + whitelist: List[str] + length: int + method: List[str] + + + +class DeleteTrade(BaseModel): + cancel_order_count: int + result: str + result_msg: str + trade_id: int diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index 961eb2c78..00964f162 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from fastapi import APIRouter, Depends @@ -6,8 +6,8 @@ from freqtrade import __version__ from freqtrade.rpc import RPC from freqtrade.rpc.rpc import RPCException -from .api_models import (Balances, Count, ForceBuyPayload, ForceSellPayload, PerformanceEntry, Ping, Profit, Stats, - StatusMsg, Version) +from .api_models import (Balances, BlacklistPayload, BlacklistResponse, Count, Daily, DeleteTrade, ForceBuyPayload, ForceSellPayload, Locks, Logs, PerformanceEntry, Ping, Profit, ResultMsg, Stats, + StatusMsg, Version, WhitelistResponse) from .deps import get_config, get_rpc @@ -55,6 +55,12 @@ def stats(rpc: RPC = Depends(get_rpc)): return rpc._rpc_stats() +@router.get('/daily', response_model=Daily, tags=['info']) +def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)): + return rpc._rpc_daily_profit(timescale, config['stake_currency'], + config.get('fiat_display_currency', '')) + + # TODO: Missing response model @router.get('/status', tags=['info']) def status(rpc: RPC = Depends(get_rpc)): @@ -64,10 +70,30 @@ def status(rpc: RPC = Depends(get_rpc)): return [] +# TODO: Missing response model +@router.get('/trades', tags=['info']) +def trades(limit: Optional[int] = 0, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_trade_history(limit) + + +@router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading']) +def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_delete(tradeid) + + +# TODO: Missing response model +@router.get('/edge', tags=['info']) +def edge(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_edge() + + +# TODO: Missing response model @router.get('/show_config', tags=['info']) def show_config(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): return RPC._rpc_show_config(config, rpc._freqtrade.state) + +# TODO: Missing response model @router.post('/forcebuy', tags=['trading']) def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): trade = rpc._rpc_forcebuy(payload.pair, payload.price) @@ -78,11 +104,36 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): return {"status": f"Error buying pair {payload.pair}."} -@router.post('/forcesell', tags=['trading']) +@router.post('/forcesell', response_model=ResultMsg, tags=['trading']) def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)): return rpc._rpc_forcesell(payload.tradeid) +@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) +def blacklist(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_blacklist() + + +@router.post('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) +def blacklist_post(payload: BlacklistPayload, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_blacklist(payload.blacklist) + + +@router.get('/whitelist', response_model=WhitelistResponse, tags=['info', 'pairlist']) +def whitelist(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_whitelist() + + +@router.get('/locks', response_model=Locks, tags=['info']) +def locks(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_locks() + + +@router.get('/logs', response_model=Logs, tags=['info']) +def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_get_logs(limit) + + @router.post('/start', response_model=StatusMsg, tags=['botcontrol']) def start(rpc: RPC = Depends(get_rpc)): return rpc._rpc_start() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b700714a8..49ca4aa9c 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -337,10 +337,10 @@ def test_api_locks(botclient): rc = client_get(client, f"{BASE_URI}/locks") assert_response(rc) - assert 'locks' in rc.json + assert 'locks' in rc.json() - assert rc.json['lock_count'] == 0 - assert rc.json['lock_count'] == len(rc.json['locks']) + assert rc.json()['lock_count'] == 0 + assert rc.json()['lock_count'] == len(rc.json()['locks']) PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason') PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef') @@ -348,11 +348,11 @@ def test_api_locks(botclient): rc = client_get(client, f"{BASE_URI}/locks") assert_response(rc) - assert rc.json['lock_count'] == 2 - assert rc.json['lock_count'] == len(rc.json['locks']) - assert 'ETH/BTC' in (rc.json['locks'][0]['pair'], rc.json['locks'][1]['pair']) - assert 'randreason' in (rc.json['locks'][0]['reason'], rc.json['locks'][1]['reason']) - assert 'deadbeef' in (rc.json['locks'][0]['reason'], rc.json['locks'][1]['reason']) + assert rc.json()['lock_count'] == 2 + assert rc.json()['lock_count'] == len(rc.json()['locks']) + assert 'ETH/BTC' in (rc.json()['locks'][0]['pair'], rc.json()['locks'][1]['pair']) + assert 'randreason' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason']) + assert 'deadbeef' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason']) def test_api_show_config(botclient, mocker): @@ -384,10 +384,10 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): ) rc = client_get(client, f"{BASE_URI}/daily") assert_response(rc) - assert len(rc.json['data']) == 7 - assert rc.json['stake_currency'] == 'BTC' - assert rc.json['fiat_display_currency'] == 'USD' - assert rc.json['data'][0]['date'] == str(datetime.utcnow().date()) + assert len(rc.json()['data']) == 7 + assert rc.json()['stake_currency'] == 'BTC' + assert rc.json()['fiat_display_currency'] == 'USD' + assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date()) def test_api_trades(botclient, mocker, fee, markets): @@ -399,19 +399,20 @@ def test_api_trades(botclient, mocker, fee, markets): ) rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) - assert len(rc.json) == 2 - assert rc.json['trades_count'] == 0 + assert len(rc.json()) == 2 + assert rc.json()['trades_count'] == 0 create_mock_trades(fee) + Trade.session.flush() rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) - assert len(rc.json['trades']) == 2 - assert rc.json['trades_count'] == 2 + assert len(rc.json()['trades']) == 2 + assert rc.json()['trades_count'] == 2 rc = client_get(client, f"{BASE_URI}/trades?limit=1") assert_response(rc) - assert len(rc.json['trades']) == 1 - assert rc.json['trades_count'] == 1 + assert len(rc.json()['trades']) == 1 + assert rc.json()['trades_count'] == 1 def test_api_delete_trade(botclient, mocker, fee, markets): @@ -430,6 +431,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets): assert_response(rc, 502) create_mock_trades(fee) + Trade.session.flush() ftbot.strategy.order_types['stoploss_on_exchange'] = True trades = Trade.query.all() trades[1].stoploss_order_id = '1234' @@ -437,7 +439,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets): rc = client_delete(client, f"{BASE_URI}/trades/1") assert_response(rc) - assert rc.json['result_msg'] == 'Deleted trade 1. Closed 1 open orders.' + assert rc.json()['result_msg'] == 'Deleted trade 1. Closed 1 open orders.' assert len(trades) - 1 == len(Trade.query.all()) assert cancel_mock.call_count == 1 @@ -450,7 +452,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets): assert len(trades) - 1 == len(Trade.query.all()) rc = client_delete(client, f"{BASE_URI}/trades/2") assert_response(rc) - assert rc.json['result_msg'] == 'Deleted trade 2. Closed 2 open orders.' + assert rc.json()['result_msg'] == 'Deleted trade 2. Closed 2 open orders.' assert len(trades) - 2 == len(Trade.query.all()) assert stoploss_mock.call_count == 1 @@ -459,28 +461,28 @@ def test_api_logs(botclient): ftbot, client = botclient rc = client_get(client, f"{BASE_URI}/logs") assert_response(rc) - assert len(rc.json) == 2 - assert 'logs' in rc.json + assert len(rc.json()) == 2 + assert 'logs' in rc.json() # Using a fixed comparison here would make this test fail! - assert rc.json['log_count'] > 1 - assert len(rc.json['logs']) == rc.json['log_count'] + assert rc.json()['log_count'] > 1 + assert len(rc.json()['logs']) == rc.json()['log_count'] - assert isinstance(rc.json['logs'][0], list) + assert isinstance(rc.json()['logs'][0], list) # date - assert isinstance(rc.json['logs'][0][0], str) + assert isinstance(rc.json()['logs'][0][0], str) # created_timestamp - assert isinstance(rc.json['logs'][0][1], float) - assert isinstance(rc.json['logs'][0][2], str) - assert isinstance(rc.json['logs'][0][3], str) - assert isinstance(rc.json['logs'][0][4], str) + assert isinstance(rc.json()['logs'][0][1], float) + assert isinstance(rc.json()['logs'][0][2], str) + assert isinstance(rc.json()['logs'][0][3], str) + assert isinstance(rc.json()['logs'][0][4], str) rc = client_get(client, f"{BASE_URI}/logs?limit=5") assert_response(rc) - assert len(rc.json) == 2 - assert 'logs' in rc.json + assert len(rc.json()) == 2 + assert 'logs' in rc.json() # Using a fixed comparison here would make this test fail! - assert rc.json['log_count'] == 5 - assert len(rc.json['logs']) == rc.json['log_count'] + assert rc.json()['log_count'] == 5 + assert len(rc.json()['logs']) == rc.json()['log_count'] def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): @@ -495,7 +497,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): ) rc = client_get(client, f"{BASE_URI}/edge") assert_response(rc, 502) - assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} + assert rc.json() == {"error": "Error querying /api/v1/edge: Edge is not enabled."} @pytest.mark.usefixtures("init_persistence") @@ -647,7 +649,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/status") assert_response(rc, 200) - assert rc.json == [] + assert rc.json() == [] ftbot.enter_positions() trades = Trade.get_open_trades() @@ -656,67 +658,68 @@ def test_api_status(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) - assert len(rc.json) == 1 - assert rc.json == [{'amount': 91.07468123, - 'amount_requested': 91.07468123, - 'base_currency': 'BTC', - 'close_date': None, - 'close_date_hum': None, - 'close_timestamp': None, - 'close_profit': None, - 'close_profit_pct': None, - 'close_profit_abs': None, - 'close_rate': None, - 'current_profit': -0.00408133, - 'current_profit_pct': -0.41, - 'current_profit_abs': -4.09e-06, - 'profit_ratio': -0.00408133, - 'profit_pct': -0.41, - 'profit_abs': -4.09e-06, - 'current_rate': 1.099e-05, - 'open_date': ANY, - 'open_date_hum': 'just now', - 'open_timestamp': ANY, - 'open_order': None, - 'open_rate': 1.098e-05, - 'pair': 'ETH/BTC', - 'stake_amount': 0.001, - 'stop_loss_abs': 9.882e-06, - 'stop_loss_pct': -10.0, - 'stop_loss_ratio': -0.1, - 'stoploss_order_id': None, - 'stoploss_last_update': ANY, - 'stoploss_last_update_timestamp': ANY, - 'initial_stop_loss_abs': 9.882e-06, - 'initial_stop_loss_pct': -10.0, - 'initial_stop_loss_ratio': -0.1, - 'stoploss_current_dist': -1.1080000000000002e-06, - 'stoploss_current_dist_ratio': -0.10081893, - 'stoploss_current_dist_pct': -10.08, - 'stoploss_entry_dist': -0.00010475, - 'stoploss_entry_dist_ratio': -0.10448878, - 'trade_id': 1, - 'close_rate_requested': None, - 'current_rate': 1.099e-05, - 'fee_close': 0.0025, - 'fee_close_cost': None, - 'fee_close_currency': None, - 'fee_open': 0.0025, - 'fee_open_cost': None, - 'fee_open_currency': None, - 'open_date': ANY, - 'is_open': True, - 'max_rate': 1.099e-05, - 'min_rate': 1.098e-05, - 'open_order_id': None, - 'open_rate_requested': 1.098e-05, - 'open_trade_value': 0.0010025, - 'sell_reason': None, - 'sell_order_status': None, - 'strategy': 'DefaultStrategy', - 'timeframe': 5, - 'exchange': 'bittrex', - }] + assert len(rc.json()) == 1 + assert rc.json() == [{ + 'amount': 91.07468123, + 'amount_requested': 91.07468123, + 'base_currency': 'BTC', + 'close_date': None, + 'close_date_hum': None, + 'close_timestamp': None, + 'close_profit': None, + 'close_profit_pct': None, + 'close_profit_abs': None, + 'close_rate': None, + 'current_profit': -0.00408133, + 'current_profit_pct': -0.41, + 'current_profit_abs': -4.09e-06, + 'profit_ratio': -0.00408133, + 'profit_pct': -0.41, + 'profit_abs': -4.09e-06, + 'current_rate': 1.099e-05, + 'open_date': ANY, + 'open_date_hum': 'just now', + 'open_timestamp': ANY, + 'open_order': None, + 'open_rate': 1.098e-05, + 'pair': 'ETH/BTC', + 'stake_amount': 0.001, + 'stop_loss_abs': 9.882e-06, + 'stop_loss_pct': -10.0, + 'stop_loss_ratio': -0.1, + 'stoploss_order_id': None, + 'stoploss_last_update': ANY, + 'stoploss_last_update_timestamp': ANY, + 'initial_stop_loss_abs': 9.882e-06, + 'initial_stop_loss_pct': -10.0, + 'initial_stop_loss_ratio': -0.1, + 'stoploss_current_dist': -1.1080000000000002e-06, + 'stoploss_current_dist_ratio': -0.10081893, + 'stoploss_current_dist_pct': -10.08, + 'stoploss_entry_dist': -0.00010475, + 'stoploss_entry_dist_ratio': -0.10448878, + 'trade_id': 1, + 'close_rate_requested': None, + 'current_rate': 1.099e-05, + 'fee_close': 0.0025, + 'fee_close_cost': None, + 'fee_close_currency': None, + 'fee_open': 0.0025, + 'fee_open_cost': None, + 'fee_open_currency': None, + 'open_date': ANY, + 'is_open': True, + 'max_rate': 1.099e-05, + 'min_rate': 1.098e-05, + 'open_order_id': None, + 'open_rate_requested': 1.098e-05, + 'open_trade_value': 0.0010025, + 'sell_reason': None, + 'sell_order_status': None, + 'strategy': 'DefaultStrategy', + 'timeframe': 5, + 'exchange': 'bittrex', + }] def test_api_version(botclient): @@ -733,33 +736,33 @@ def test_api_blacklist(botclient, mocker): rc = client_get(client, f"{BASE_URI}/blacklist") assert_response(rc) # DOGE and HOT are not in the markets mock! - assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], - "blacklist_expanded": [], - "length": 2, - "method": ["StaticPairList"], - "errors": {}, - } + assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC"], + "blacklist_expanded": [], + "length": 2, + "method": ["StaticPairList"], + "errors": {}, + } # Add ETH/BTC to blacklist rc = client_post(client, f"{BASE_URI}/blacklist", data='{"blacklist": ["ETH/BTC"]}') assert_response(rc) - assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], - "blacklist_expanded": ["ETH/BTC"], - "length": 3, - "method": ["StaticPairList"], - "errors": {}, - } + assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], + "blacklist_expanded": ["ETH/BTC"], + "length": 3, + "method": ["StaticPairList"], + "errors": {}, + } rc = client_post(client, f"{BASE_URI}/blacklist", data='{"blacklist": ["XRP/.*"]}') assert_response(rc) - assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"], - "blacklist_expanded": ["ETH/BTC", "XRP/BTC"], - "length": 4, - "method": ["StaticPairList"], - "errors": {}, - } + assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"], + "blacklist_expanded": ["ETH/BTC", "XRP/BTC"], + "length": 4, + "method": ["StaticPairList"], + "errors": {}, + } def test_api_whitelist(botclient): @@ -767,9 +770,11 @@ def test_api_whitelist(botclient): rc = client_get(client, f"{BASE_URI}/whitelist") assert_response(rc) - assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], - "length": 4, - "method": ["StaticPairList"]} + assert rc.json() == { + "whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], + "length": 4, + "method": ["StaticPairList"] + } def test_api_forcebuy(botclient, mocker, fee): @@ -778,7 +783,7 @@ def test_api_forcebuy(botclient, mocker, fee): rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc, 502) - assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} + assert rc.json() == {"error": "Error querying /api/v1/forcebuy: Forcebuy not enabled."} # enable forcebuy ftbot.config['forcebuy_enable'] = True @@ -788,9 +793,9 @@ def test_api_forcebuy(botclient, mocker, fee): rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc) - assert rc.json == {"status": "Error buying pair ETH/BTC."} + assert rc.json() == {"status": "Error buying pair ETH/BTC."} - # Test creating trae + # Test creating trade fbuy_mock = MagicMock(return_value=Trade( pair='ETH/ETH', amount=1, @@ -810,53 +815,54 @@ def test_api_forcebuy(botclient, mocker, fee): rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc) - assert rc.json == {'amount': 1, - 'amount_requested': 1, - 'trade_id': None, - 'close_date': None, - 'close_date_hum': None, - 'close_timestamp': None, - 'close_rate': 0.265441, - 'open_date': ANY, - 'open_date_hum': 'just now', - 'open_timestamp': ANY, - 'open_rate': 0.245441, - 'pair': 'ETH/ETH', - 'stake_amount': 1, - 'stop_loss_abs': None, - 'stop_loss_pct': None, - 'stop_loss_ratio': None, - 'stoploss_order_id': None, - 'stoploss_last_update': None, - 'stoploss_last_update_timestamp': None, - 'initial_stop_loss_abs': None, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, - 'close_profit': None, - 'close_profit_pct': None, - 'close_profit_abs': None, - 'close_rate_requested': None, - 'profit_ratio': None, - 'profit_pct': None, - 'profit_abs': None, - 'fee_close': 0.0025, - 'fee_close_cost': None, - 'fee_close_currency': None, - 'fee_open': 0.0025, - 'fee_open_cost': None, - 'fee_open_currency': None, - 'is_open': False, - 'max_rate': None, - 'min_rate': None, - 'open_order_id': '123456', - 'open_rate_requested': None, - 'open_trade_value': 0.24605460, - 'sell_reason': None, - 'sell_order_status': None, - 'strategy': None, - 'timeframe': None, - 'exchange': 'bittrex', - } + assert rc.json() == { + 'amount': 1, + 'amount_requested': 1, + 'trade_id': None, + 'close_date': None, + 'close_date_hum': None, + 'close_timestamp': None, + 'close_rate': 0.265441, + 'open_date': ANY, + 'open_date_hum': 'just now', + 'open_timestamp': ANY, + 'open_rate': 0.245441, + 'pair': 'ETH/ETH', + 'stake_amount': 1, + 'stop_loss_abs': None, + 'stop_loss_pct': None, + 'stop_loss_ratio': None, + 'stoploss_order_id': None, + 'stoploss_last_update': None, + 'stoploss_last_update_timestamp': None, + 'initial_stop_loss_abs': None, + 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, + 'close_profit': None, + 'close_profit_pct': None, + 'close_profit_abs': None, + 'close_rate_requested': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, + 'fee_close': 0.0025, + 'fee_close_cost': None, + 'fee_close_currency': None, + 'fee_open': 0.0025, + 'fee_open_cost': None, + 'fee_open_currency': None, + 'is_open': False, + 'max_rate': None, + 'min_rate': None, + 'open_order_id': '123456', + 'open_rate_requested': None, + 'open_trade_value': 0.24605460, + 'sell_reason': None, + 'sell_order_status': None, + 'strategy': None, + 'timeframe': None, + 'exchange': 'bittrex', + } def test_api_forcesell(botclient, mocker, ticker, fee, markets): @@ -873,14 +879,14 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') assert_response(rc, 502) - assert rc.json == {"error": "Error querying _forcesell: invalid argument"} + assert rc.json() == {"error": "Error querying /api/v1/forcesell: invalid argument"} ftbot.enter_positions() rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') assert_response(rc) - assert rc.json == {'result': 'Created sell order for trade 1.'} + assert rc.json() == {'result': 'Created sell order for trade 1.'} def test_api_pair_candles(botclient, ohlcv_history): From e23898d17bffd9d3c1d3a59e8a0ab059d4c41c85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Dec 2020 17:48:19 +0100 Subject: [PATCH 099/563] Improve some tests --- freqtrade/rpc/api_server2/api_models.py | 1 - freqtrade/rpc/api_server2/api_v1.py | 6 ++++-- tests/rpc/test_rpc_apiserver.py | 25 +++++++++++-------------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_models.py b/freqtrade/rpc/api_server2/api_models.py index a30973845..bcb3c280e 100644 --- a/freqtrade/rpc/api_server2/api_models.py +++ b/freqtrade/rpc/api_server2/api_models.py @@ -156,7 +156,6 @@ class WhitelistResponse(BaseModel): method: List[str] - class DeleteTrade(BaseModel): cancel_order_count: int result: str diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index 00964f162..8cfc85a55 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -19,17 +19,19 @@ router = APIRouter() @router_public.get('/ping', response_model=Ping) def ping(): - """simple ping version""" + """simple ping""" return {"status": "pong"} @router.get('/version', response_model=Version, tags=['info']) def version(): + """ Bot Version info""" return {"version": __version__} @router.get('/balance', response_model=Balances, tags=['info']) def balance(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): + """Account Balances""" return rpc._rpc_balance(config['stake_currency'], config.get('fiat_display_currency', ''),) @@ -71,7 +73,7 @@ def status(rpc: RPC = Depends(get_rpc)): # TODO: Missing response model -@router.get('/trades', tags=['info']) +@router.get('/trades', tags=['info', 'trading']) def trades(limit: Optional[int] = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 49ca4aa9c..6ff1b3e95 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -80,10 +80,7 @@ def test_api_not_found(botclient): rc = client_post(client, f"{BASE_URI}/invalid_url") assert_response(rc, 404) - assert rc.json() == {"status": "error", - "reason": f"There's no API call for http://localhost{BASE_URI}/invalid_url.", - "code": 404 - } + assert rc.json() == {"detail": "Not Found"} def test_api_unauthorized(botclient): @@ -137,13 +134,12 @@ def test_api_token_refresh(botclient): rc = client_post(client, f"{BASE_URI}/token/login") assert_response(rc) rc = client.post(f"{BASE_URI}/token/refresh", - content_type="application/json", data=None, - headers={'Authorization': f'Bearer {rc.json["refresh_token"]}', + headers={'Authorization': f'Bearer {rc.json()["refresh_token"]}', 'Origin': 'http://example.com'}) assert_response(rc) - assert 'access_token' in rc.json - assert 'refresh_token' not in rc.json + assert 'access_token' in rc.json() + assert 'refresh_token' not in rc.json() def test_api_stop_workflow(botclient): @@ -151,24 +147,24 @@ def test_api_stop_workflow(botclient): assert ftbot.state == State.RUNNING rc = client_post(client, f"{BASE_URI}/stop") assert_response(rc) - assert rc.json == {'status': 'stopping trader ...'} + assert rc.json() == {'status': 'stopping trader ...'} assert ftbot.state == State.STOPPED # Stop bot again rc = client_post(client, f"{BASE_URI}/stop") assert_response(rc) - assert rc.json == {'status': 'already stopped'} + assert rc.json() == {'status': 'already stopped'} # Start bot rc = client_post(client, f"{BASE_URI}/start") assert_response(rc) - assert rc.json == {'status': 'starting trader ...'} + assert rc.json() == {'status': 'starting trader ...'} assert ftbot.state == State.RUNNING # Call start again rc = client_post(client, f"{BASE_URI}/start") assert_response(rc) - assert rc.json == {'status': 'already running'} + assert rc.json() == {'status': 'already running'} def test_api__init__(default_conf, mocker): @@ -182,7 +178,7 @@ def test_api__init__(default_conf, mocker): "password": "testPass", }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) - mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) + mocker.patch('freqtrade.rpc.api_server2.webserver.ApiServer.start_api', MagicMock()) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) assert apiserver._config == default_conf @@ -255,7 +251,7 @@ def test_api_cleanup(default_conf, mocker, caplog): mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) - apiserver.run() + apiserver.run_api() stop_mock = MagicMock() stop_mock.shutdown = MagicMock() apiserver.srv = stop_mock @@ -655,6 +651,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): trades = Trade.get_open_trades() trades[0].open_order_id = None ftbot.exit_positions(trades) + Trade.session.flush() rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) From 9350f505bcee3bb5855d4d7da67c9f210fee3528 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Dec 2020 20:05:27 +0100 Subject: [PATCH 100/563] Implement missing methods --- freqtrade/rpc/api_server2/api_models.py | 22 ++++- freqtrade/rpc/api_server2/api_v1.py | 88 ++++++++++++++++++- freqtrade/rpc/api_server2/webserver.py | 3 +- tests/rpc/test_rpc_apiserver.py | 110 ++++++++++++------------ 4 files changed, 162 insertions(+), 61 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_models.py b/freqtrade/rpc/api_server2/api_models.py index bcb3c280e..2e96dca40 100644 --- a/freqtrade/rpc/api_server2/api_models.py +++ b/freqtrade/rpc/api_server2/api_models.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union from datetime import date from pydantic import BaseModel @@ -161,3 +161,23 @@ class DeleteTrade(BaseModel): result: str result_msg: str trade_id: int + + +class PlotConfig(BaseModel): + main_plot: Optional[Dict[str, Any]] + subplots: Optional[Dict[str, Any]] + + +class StrategyListResponse(BaseModel): + strategies: List[str] + + +class StrategyResponse(BaseModel): + strategy: str + code: str + + +class AvailablePairs(BaseModel): + length: int + pairs: List[str] + pair_interval: List[List[str]] diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index 8cfc85a55..8389b7336 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -1,13 +1,21 @@ -from typing import List, Optional +from copy import deepcopy +from freqtrade.constants import USERPATH_STRATEGIES +from typing import Dict, List, Optional, Union +from pathlib import Path from fastapi import APIRouter, Depends +from fastapi.exceptions import HTTPException from freqtrade import __version__ +from freqtrade.data.history import get_datahandler +from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC from freqtrade.rpc.rpc import RPCException -from .api_models import (Balances, BlacklistPayload, BlacklistResponse, Count, Daily, DeleteTrade, ForceBuyPayload, ForceSellPayload, Locks, Logs, PerformanceEntry, Ping, Profit, ResultMsg, Stats, - StatusMsg, Version, WhitelistResponse) +from .api_models import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, + Daily, DeleteTrade, ForceBuyPayload, ForceSellPayload, Locks, Logs, + PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, Stats, StatusMsg, StrategyListResponse, StrategyResponse, Version, + WhitelistResponse) from .deps import get_config, get_rpc @@ -154,3 +162,77 @@ def stop_buy(rpc: RPC = Depends(get_rpc)): @router.post('/reload_config', response_model=StatusMsg, tags=['botcontrol']) def reload_config(rpc: RPC = Depends(get_rpc)): return rpc._rpc_reload_config() + + +# TODO: Missing response model +@router.get('/pair_candles', tags=['candle data']) +def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc=Depends(get_rpc)): + return rpc._rpc_analysed_dataframe(pair, timeframe, limit) + + +# TODO: Missing response model +@router.get('/pair_history', tags=['candle data']) +def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, + config=Depends(get_config)): + config = deepcopy(config) + config.update({ + 'strategy': strategy, + }) + return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) + + +@router.get('/plot_config', response_model=Union[Dict, PlotConfig], tags=['candle data']) +def plot_config(rpc=Depends(get_rpc)): + return rpc._rpc_plot_config() + + +@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy']) +def list_strategies(config=Depends(get_config)): + directory = Path(config.get( + 'strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES)) + from freqtrade.resolvers.strategy_resolver import StrategyResolver + strategies = StrategyResolver.search_all_objects(directory, False) + strategies = sorted(strategies, key=lambda x: x['name']) + + return {'strategies': [x['name'] for x in strategies]} + + +@router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy']) +def get_strategy(strategy: str, rpc: RPC = Depends(get_rpc), config=Depends(get_config)): + + config = deepcopy(config) + from freqtrade.resolvers.strategy_resolver import StrategyResolver + try: + strategy_obj = StrategyResolver._load_strategy(strategy, config, + extra_dir=config.get('strategy_path')) + except OperationalException: + raise HTTPException(status_code=404, detail='Strategy not found') + + return { + 'strategy': strategy_obj.get_strategy_name(), + 'code': strategy_obj.__source__, + } + + +@router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data']) +def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None, + config=Depends(get_config)): + + dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', None)) + + pair_interval = dh.ohlcv_get_available_data(config['datadir']) + + if timeframe: + pair_interval = [pair for pair in pair_interval if pair[1] == timeframe] + if stake_currency: + pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)] + pair_interval = sorted(pair_interval, key=lambda x: x[0]) + + pairs = list({x[0] for x in pair_interval}) + + result = { + 'length': len(pairs), + 'pairs': pairs, + 'pair_interval': pair_interval, + } + return result diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index 45e695151..f54845535 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -1,11 +1,10 @@ import logging from typing import Any, Dict, Optional -from starlette.responses import JSONResponse import uvicorn from fastapi import Depends, FastAPI -from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware +from starlette.responses import JSONResponse from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 6ff1b3e95..4ebe63f46 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -894,22 +894,22 @@ def test_api_pair_candles(botclient, ohlcv_history): # No pair rc = client_get(client, f"{BASE_URI}/pair_candles?limit={amount}&timeframe={timeframe}") - assert_response(rc, 400) + assert_response(rc, 422) # No timeframe rc = client_get(client, f"{BASE_URI}/pair_candles?pair=XRP%2FBTC") - assert_response(rc, 400) + assert_response(rc, 422) rc = client_get(client, f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") assert_response(rc) - assert 'columns' in rc.json - assert 'data_start_ts' in rc.json - assert 'data_start' in rc.json - assert 'data_stop' in rc.json - assert 'data_stop_ts' in rc.json - assert len(rc.json['data']) == 0 + assert 'columns' in rc.json() + assert 'data_start_ts' in rc.json() + assert 'data_start' in rc.json() + assert 'data_stop' in rc.json() + assert 'data_stop_ts' in rc.json() + assert len(rc.json()['data']) == 0 ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean() ohlcv_history['buy'] = 0 ohlcv_history.loc[1, 'buy'] = 1 @@ -920,28 +920,28 @@ def test_api_pair_candles(botclient, ohlcv_history): rc = client_get(client, f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") assert_response(rc) - assert 'strategy' in rc.json - assert rc.json['strategy'] == 'DefaultStrategy' - assert 'columns' in rc.json - assert 'data_start_ts' in rc.json - assert 'data_start' in rc.json - assert 'data_stop' in rc.json - assert 'data_stop_ts' in rc.json - assert rc.json['data_start'] == '2017-11-26 08:50:00+00:00' - assert rc.json['data_start_ts'] == 1511686200000 - assert rc.json['data_stop'] == '2017-11-26 09:00:00+00:00' - assert rc.json['data_stop_ts'] == 1511686800000 - assert isinstance(rc.json['columns'], list) - assert rc.json['columns'] == ['date', 'open', 'high', + assert 'strategy' in rc.json() + assert rc.json()['strategy'] == 'DefaultStrategy' + assert 'columns' in rc.json() + assert 'data_start_ts' in rc.json() + assert 'data_start' in rc.json() + assert 'data_stop' in rc.json() + assert 'data_stop_ts' in rc.json() + assert rc.json()['data_start'] == '2017-11-26 08:50:00+00:00' + assert rc.json()['data_start_ts'] == 1511686200000 + assert rc.json()['data_stop'] == '2017-11-26 09:00:00+00:00' + assert rc.json()['data_stop_ts'] == 1511686800000 + assert isinstance(rc.json()['columns'], list) + assert rc.json()['columns'] == ['date', 'open', 'high', 'low', 'close', 'volume', 'sma', 'buy', 'sell', '__date_ts', '_buy_signal_open', '_sell_signal_open'] - assert 'pair' in rc.json - assert rc.json['pair'] == 'XRP/BTC' + assert 'pair' in rc.json() + assert rc.json()['pair'] == 'XRP/BTC' - assert 'data' in rc.json - assert len(rc.json['data']) == amount + assert 'data' in rc.json() + assert len(rc.json()['data']) == amount - assert (rc.json['data'] == + assert (rc.json()['data'] == [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, None, 0, 0, 1511686200000, None, None], ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, @@ -960,41 +960,41 @@ def test_api_pair_history(botclient, ohlcv_history): rc = client_get(client, f"{BASE_URI}/pair_history?timeframe={timeframe}" "&timerange=20180111-20180112&strategy=DefaultStrategy") - assert_response(rc, 400) + assert_response(rc, 422) # No Timeframe rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC" "&timerange=20180111-20180112&strategy=DefaultStrategy") - assert_response(rc, 400) + assert_response(rc, 422) # No timerange rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" "&strategy=DefaultStrategy") - assert_response(rc, 400) + assert_response(rc, 422) # No strategy rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" "&timerange=20180111-20180112") - assert_response(rc, 400) + assert_response(rc, 422) # Working rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" "&timerange=20180111-20180112&strategy=DefaultStrategy") assert_response(rc, 200) - assert rc.json['length'] == 289 - assert len(rc.json['data']) == rc.json['length'] - assert 'columns' in rc.json - assert 'data' in rc.json - assert rc.json['pair'] == 'UNITTEST/BTC' - assert rc.json['strategy'] == 'DefaultStrategy' - assert rc.json['data_start'] == '2018-01-11 00:00:00+00:00' - assert rc.json['data_start_ts'] == 1515628800000 - assert rc.json['data_stop'] == '2018-01-12 00:00:00+00:00' - assert rc.json['data_stop_ts'] == 1515715200000 + assert rc.json()['length'] == 289 + assert len(rc.json()['data']) == rc.json()['length'] + assert 'columns' in rc.json() + assert 'data' in rc.json() + assert rc.json()['pair'] == 'UNITTEST/BTC' + assert rc.json()['strategy'] == 'DefaultStrategy' + assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00' + assert rc.json()['data_start_ts'] == 1515628800000 + assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00' + assert rc.json()['data_stop_ts'] == 1515715200000 def test_api_plot_config(botclient): @@ -1002,14 +1002,14 @@ def test_api_plot_config(botclient): rc = client_get(client, f"{BASE_URI}/plot_config") assert_response(rc) - assert rc.json == {} + assert rc.json() == {} ftbot.strategy.plot_config = {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}} rc = client_get(client, f"{BASE_URI}/plot_config") assert_response(rc) - assert rc.json == ftbot.strategy.plot_config - assert isinstance(rc.json['main_plot'], dict) + assert rc.json() == ftbot.strategy.plot_config + assert isinstance(rc.json()['main_plot'], dict) def test_api_strategies(botclient): @@ -1018,7 +1018,7 @@ def test_api_strategies(botclient): rc = client_get(client, f"{BASE_URI}/strategies") assert_response(rc) - assert rc.json == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} + assert rc.json() == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} def test_api_strategy(botclient): @@ -1027,10 +1027,10 @@ def test_api_strategy(botclient): rc = client_get(client, f"{BASE_URI}/strategy/DefaultStrategy") assert_response(rc) - assert rc.json['strategy'] == 'DefaultStrategy' + assert rc.json()['strategy'] == 'DefaultStrategy' data = (Path(__file__).parents[1] / "strategy/strats/default_strategy.py").read_text() - assert rc.json['code'] == data + assert rc.json()['code'] == data rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") assert_response(rc, 404) @@ -1042,21 +1042,21 @@ def test_list_available_pairs(botclient): rc = client_get(client, f"{BASE_URI}/available_pairs") assert_response(rc) - assert rc.json['length'] == 12 - assert isinstance(rc.json['pairs'], list) + assert rc.json()['length'] == 12 + assert isinstance(rc.json()['pairs'], list) rc = client_get(client, f"{BASE_URI}/available_pairs?timeframe=5m") assert_response(rc) - assert rc.json['length'] == 12 + assert rc.json()['length'] == 12 rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH") assert_response(rc) - assert rc.json['length'] == 1 - assert rc.json['pairs'] == ['XRP/ETH'] - assert len(rc.json['pair_interval']) == 2 + assert rc.json()['length'] == 1 + assert rc.json()['pairs'] == ['XRP/ETH'] + assert len(rc.json()['pair_interval']) == 2 rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH&timeframe=5m") assert_response(rc) - assert rc.json['length'] == 1 - assert rc.json['pairs'] == ['XRP/ETH'] - assert len(rc.json['pair_interval']) == 1 + assert rc.json()['length'] == 1 + assert rc.json()['pairs'] == ['XRP/ETH'] + assert len(rc.json()['pair_interval']) == 1 From 9f873305ebe6a2a5d0e4382c1277b1d9a0ebabc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Dec 2020 09:02:35 +0100 Subject: [PATCH 101/563] Improve response models --- freqtrade/rpc/api_server2/api_auth.py | 3 +-- freqtrade/rpc/api_server2/api_models.py | 28 ++++++++++++++++++++++++- freqtrade/rpc/api_server2/api_v1.py | 13 ++++++------ tests/rpc/test_rpc_apiserver.py | 4 ++-- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_auth.py b/freqtrade/rpc/api_server2/api_auth.py index cb19d7637..599f6b53c 100644 --- a/freqtrade/rpc/api_server2/api_auth.py +++ b/freqtrade/rpc/api_server2/api_auth.py @@ -2,10 +2,9 @@ import secrets from datetime import datetime, timedelta import jwt -from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from fastapi.security.http import HTTPBasic, HTTPBasicCredentials -from pydantic import BaseModel from freqtrade.rpc.api_server2.api_models import AccessAndRefreshToken, AccessToken diff --git a/freqtrade/rpc/api_server2/api_models.py b/freqtrade/rpc/api_server2/api_models.py index 2e96dca40..c9e4ee5cc 100644 --- a/freqtrade/rpc/api_server2/api_models.py +++ b/freqtrade/rpc/api_server2/api_models.py @@ -1,7 +1,10 @@ +from datetime import date, datetime from typing import Any, Dict, List, Optional, Union -from datetime import date + from pydantic import BaseModel +from freqtrade.constants import DATETIME_PRINT_FORMAT + class Ping(BaseModel): status: str @@ -181,3 +184,26 @@ class AvailablePairs(BaseModel): length: int pairs: List[str] pair_interval: List[List[str]] + + +class PairHistory(BaseModel): + strategy: str + pair: str + timeframe: str + timeframe_ms: int + columns: List[str] + data: List[Any] + length: int + buy_signals: int + sell_signals: int + last_analyzed: datetime + last_analyzed_ts: int + data_start_ts: int + data_start: str + data_stop: str + data_stop_ts: int + + class Config: + json_encoders = { + datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT), + } diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server2/api_v1.py index 8389b7336..21c525850 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server2/api_v1.py @@ -1,12 +1,12 @@ from copy import deepcopy -from freqtrade.constants import USERPATH_STRATEGIES -from typing import Dict, List, Optional, Union from pathlib import Path +from typing import Dict, List, Optional, Union from fastapi import APIRouter, Depends from fastapi.exceptions import HTTPException from freqtrade import __version__ +from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.data.history import get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC @@ -14,7 +14,8 @@ from freqtrade.rpc.rpc import RPCException from .api_models import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, Daily, DeleteTrade, ForceBuyPayload, ForceSellPayload, Locks, Logs, - PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, Stats, StatusMsg, StrategyListResponse, StrategyResponse, Version, + PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, Stats, + StatusMsg, StrategyListResponse, StrategyResponse, Version, WhitelistResponse) from .deps import get_config, get_rpc @@ -164,14 +165,12 @@ def reload_config(rpc: RPC = Depends(get_rpc)): return rpc._rpc_reload_config() -# TODO: Missing response model -@router.get('/pair_candles', tags=['candle data']) +@router.get('/pair_candles', response_model=PairHistory, tags=['candle data']) def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc=Depends(get_rpc)): return rpc._rpc_analysed_dataframe(pair, timeframe, limit) -# TODO: Missing response model -@router.get('/pair_history', tags=['candle data']) +@router.get('/pair_history', response_model=PairHistory, tags=['candle data']) def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, config=Depends(get_config)): config = deepcopy(config) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 4ebe63f46..7d1100fb8 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -933,8 +933,8 @@ def test_api_pair_candles(botclient, ohlcv_history): assert rc.json()['data_stop_ts'] == 1511686800000 assert isinstance(rc.json()['columns'], list) assert rc.json()['columns'] == ['date', 'open', 'high', - 'low', 'close', 'volume', 'sma', 'buy', 'sell', - '__date_ts', '_buy_signal_open', '_sell_signal_open'] + 'low', 'close', 'volume', 'sma', 'buy', 'sell', + '__date_ts', '_buy_signal_open', '_sell_signal_open'] assert 'pair' in rc.json() assert rc.json()['pair'] == 'XRP/BTC' From 54a50b1fb4b3dd0fc405271db6c620a7bde412d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Dec 2020 10:56:19 +0100 Subject: [PATCH 102/563] Fix some tests --- freqtrade/rpc/api_server2/uvicorn_threaded.py | 3 ++ freqtrade/rpc/api_server2/webserver.py | 33 ++++++++++---- tests/rpc/test_rpc_apiserver.py | 44 +++++++++---------- tests/rpc/test_rpc_manager.py | 4 +- 4 files changed, 51 insertions(+), 33 deletions(-) diff --git a/freqtrade/rpc/api_server2/uvicorn_threaded.py b/freqtrade/rpc/api_server2/uvicorn_threaded.py index 7c8804fd3..ce7089bed 100644 --- a/freqtrade/rpc/api_server2/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server2/uvicorn_threaded.py @@ -10,6 +10,9 @@ class UvicornServer(uvicorn.Server): Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742 """ def install_signal_handlers(self): + """ + In the parent implementation, this starts the thread, therefore we must patch it away here. + """ pass @contextlib.contextmanager diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index f54845535..b3e6eb0dc 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -1,4 +1,5 @@ import logging +from ipaddress import IPv4Address from typing import Any, Dict, Optional import uvicorn @@ -16,7 +17,7 @@ logger = logging.getLogger(__name__) class ApiServer(RPCHandler): - _rpc: Optional[RPC] = None + _rpc: RPC = None _config: Dict[str, Any] = {} def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: @@ -34,6 +35,7 @@ class ApiServer(RPCHandler): def cleanup(self) -> None: """ Cleanup pending module resources """ if self._server: + logger.info("Stopping API Server") self._server.cleanup() def send_msg(self, msg: Dict[str, str]) -> None: @@ -71,11 +73,26 @@ class ApiServer(RPCHandler): """ Start API ... should be run in thread. """ - uvconfig = uvicorn.Config(self.app, - port=self._config['api_server'].get('listen_port', 8080), - host=self._config['api_server'].get( - 'listen_ip_address', '127.0.0.1'), - access_log=True) - self._server = UvicornServer(uvconfig) + rest_ip = self._config['api_server']['listen_ip_address'] + rest_port = self._config['api_server']['listen_port'] - self._server.run_in_thread() + logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') + if not IPv4Address(rest_ip).is_loopback: + logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") + logger.warning("SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json") + + if not self._config['api_server'].get('password'): + logger.warning("SECURITY WARNING - No password for local REST Server defined. " + "Please make sure that this is intentional!") + + logger.info('Starting Local Rest Server.') + uvconfig = uvicorn.Config(self.app, + port=rest_port, + host=rest_ip, + access_log=True) + try: + self._server = UvicornServer(uvconfig) + self._server.run_in_thread() + except Exception: + logger.exception("Api server failed to start.") diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7d1100fb8..8017293b4 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -5,17 +5,18 @@ Unit test file for rpc/api_server.py from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock +from fastapi.applications import FastAPI import pytest from fastapi.testclient import TestClient -from flask import Flask +from fastapi import FastAPI from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC -from freqtrade.rpc.api_server import BASE_URI # , ApiServer +from freqtrade.rpc.api_server111 import BASE_URI # , ApiServer from freqtrade.rpc.api_server2 import ApiServer from freqtrade.state import RunMode, State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -191,20 +192,19 @@ def test_api_run(default_conf, mocker, caplog): "password": "testPass", }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) - mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) server_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server.make_server', server_mock) + mocker.patch('freqtrade.rpc.api_server2.webserver.UvicornServer', server_mock) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) - assert apiserver._config == default_conf - apiserver.run() assert server_mock.call_count == 1 - assert server_mock.call_args_list[0][0][0] == "127.0.0.1" - assert server_mock.call_args_list[0][0][1] == 8080 - assert isinstance(server_mock.call_args_list[0][0][2], Flask) - assert hasattr(apiserver, "srv") + assert apiserver._config == default_conf + apiserver.start_api() + assert server_mock.call_count == 2 + assert server_mock.call_args_list[0][0][0].host == "127.0.0.1" + assert server_mock.call_args_list[0][0][0].port == 8080 + assert isinstance(server_mock.call_args_list[0][0][0].app, FastAPI) assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog) assert log_has("Starting Local Rest Server.", caplog) @@ -217,12 +217,12 @@ def test_api_run(default_conf, mocker, caplog): "listen_port": 8089, "password": "", }}) - apiserver.run() + apiserver.start_api() assert server_mock.call_count == 1 - assert server_mock.call_args_list[0][0][0] == "0.0.0.0" - assert server_mock.call_args_list[0][0][1] == 8089 - assert isinstance(server_mock.call_args_list[0][0][2], Flask) + assert server_mock.call_args_list[0][0][0].host == "0.0.0.0" + assert server_mock.call_args_list[0][0][0].port == 8089 + assert isinstance(server_mock.call_args_list[0][0][0].app, FastAPI) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog) assert log_has("Starting Local Rest Server.", caplog) assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", @@ -234,8 +234,8 @@ def test_api_run(default_conf, mocker, caplog): # Test crashing flask caplog.clear() - mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) - apiserver.run() + mocker.patch('freqtrade.rpc.api_server2.webserver.UvicornServer', MagicMock(side_effect=Exception)) + apiserver.start_api() assert log_has("Api server failed to start.", caplog) @@ -247,17 +247,15 @@ def test_api_cleanup(default_conf, mocker, caplog): "password": "testPass", }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) - mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) - mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) + + server_mock = MagicMock() + server_mock.cleanup = MagicMock() + mocker.patch('freqtrade.rpc.api_server2.webserver.UvicornServer', server_mock) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) - apiserver.run_api() - stop_mock = MagicMock() - stop_mock.shutdown = MagicMock() - apiserver.srv = stop_mock apiserver.cleanup() - assert stop_mock.shutdown.call_count == 1 + assert apiserver._server.cleanup.call_count == 1 assert log_has("Stopping API Server", caplog) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 06706120f..e63d629b8 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -160,7 +160,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) run_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) + mocker.patch('freqtrade.rpc.api_server2.ApiServer.start_api', run_mock) default_conf['telegram']['enabled'] = False rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) @@ -172,7 +172,7 @@ def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) run_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) + mocker.patch('freqtrade.rpc.api_server2.ApiServer.start_api', run_mock) default_conf["telegram"]["enabled"] = False default_conf["api_server"] = {"enabled": True, From 776ce57f55f912f1c7c4914982b2125b55a80327 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Dec 2020 10:59:17 +0100 Subject: [PATCH 103/563] Remove api_server --- freqtrade/rpc/api_server.py | 665 ------------------------- freqtrade/rpc/api_server2/webserver.py | 2 +- tests/rpc/test_rpc_apiserver.py | 5 +- 3 files changed, 3 insertions(+), 669 deletions(-) delete mode 100644 freqtrade/rpc/api_server.py diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py deleted file mode 100644 index b489586c8..000000000 --- a/freqtrade/rpc/api_server.py +++ /dev/null @@ -1,665 +0,0 @@ -import logging -import threading -from copy import deepcopy -from datetime import date, datetime -from ipaddress import IPv4Address -from pathlib import Path -from typing import Any, Callable, Dict - -from arrow import Arrow -from flask import Flask, jsonify, request -from flask.json import JSONEncoder -from flask_cors import CORS -from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token, - get_jwt_identity, jwt_refresh_token_required, - verify_jwt_in_request_optional) -from werkzeug.security import safe_str_cmp -from werkzeug.serving import make_server - -from freqtrade.__init__ import __version__ -from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES -from freqtrade.exceptions import OperationalException -from freqtrade.persistence import Trade -from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler - - -logger = logging.getLogger(__name__) - -BASE_URI = "/api/v1" - - -class FTJSONEncoder(JSONEncoder): - def default(self, obj): - try: - if isinstance(obj, Arrow): - return obj.for_json() - elif isinstance(obj, datetime): - return obj.strftime(DATETIME_PRINT_FORMAT) - elif isinstance(obj, date): - return obj.strftime("%Y-%m-%d") - iterable = iter(obj) - except TypeError: - pass - else: - return list(iterable) - return JSONEncoder.default(self, obj) - - -# Type should really be Callable[[ApiServer, Any], Any], but that will create a circular dependency -def require_login(func: Callable[[Any, Any], Any]): - - def func_wrapper(obj, *args, **kwargs): - verify_jwt_in_request_optional() - auth = request.authorization - if get_jwt_identity() or auth and obj.check_auth(auth.username, auth.password): - return func(obj, *args, **kwargs) - else: - return jsonify({"error": "Unauthorized"}), 401 - - return func_wrapper - - -# Type should really be Callable[[ApiServer], Any], but that will create a circular dependency -def rpc_catch_errors(func: Callable[..., Any]): - - def func_wrapper(obj, *args, **kwargs): - - try: - return func(obj, *args, **kwargs) - except RPCException as e: - logger.exception("API Error calling %s: %s", func.__name__, e) - return obj.rest_error(f"Error querying {func.__name__}: {e}") - - return func_wrapper - - -def shutdown_session(exception=None): - # Remove scoped session - Trade.session.remove() - - -class ApiServer(RPCHandler): - """ - This class runs api server and provides rpc.rpc functionality to it - - This class starts a non-blocking thread the api server runs within - """ - - def check_auth(self, username, password): - return (safe_str_cmp(username, self._config['api_server'].get('username')) and - safe_str_cmp(password, self._config['api_server'].get('password'))) - - def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: - """ - Init the api server, and init the super class RPCHandler - :param rpc: instance of RPC Helper class - :param config: Configuration object - :return: None - """ - super().__init__(rpc, config) - - self.app = Flask(__name__) - self._cors = CORS(self.app, - resources={r"/api/*": { - "supports_credentials": True, - "origins": self._config['api_server'].get('CORS_origins', [])}} - ) - - # Setup the Flask-JWT-Extended extension - self.app.config['JWT_SECRET_KEY'] = self._config['api_server'].get( - 'jwt_secret_key', 'super-secret') - - self.jwt = JWTManager(self.app) - self.app.json_encoder = FTJSONEncoder - - self.app.teardown_appcontext(shutdown_session) - - # Register application handling - self.register_rest_rpc_urls() - - thread = threading.Thread(target=self.run, daemon=True) - thread.start() - - def cleanup(self) -> None: - logger.info("Stopping API Server") - self.srv.shutdown() - - def run(self): - """ - Method that runs flask app in its own thread forever. - Section to handle configuration and running of the Rest server - also to check and warn if not bound to a loopback, warn on security risk. - """ - rest_ip = self._config['api_server']['listen_ip_address'] - rest_port = self._config['api_server']['listen_port'] - - logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') - if not IPv4Address(rest_ip).is_loopback: - logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") - logger.warning("SECURITY WARNING - This is insecure please set to your loopback," - "e.g 127.0.0.1 in config.json") - - if not self._config['api_server'].get('password'): - logger.warning("SECURITY WARNING - No password for local REST Server defined. " - "Please make sure that this is intentional!") - - # Run the Server - logger.info('Starting Local Rest Server.') - try: - self.srv = make_server(rest_ip, rest_port, self.app) - self.srv.serve_forever() - except Exception: - logger.exception("Api server failed to start.") - logger.info('Local Rest Server started.') - - def send_msg(self, msg: Dict[str, str]) -> None: - """ - We don't push to endpoints at the moment. - Take a look at webhooks for that functionality. - """ - pass - - def rest_error(self, error_msg, error_code=502): - return jsonify({"error": error_msg}), error_code - - def register_rest_rpc_urls(self): - """ - Registers flask app URLs that are calls to functionality in rpc.rpc. - - First two arguments passed are /URL and 'Label' - Label can be used as a shortcut when refactoring - :return: - """ - self.app.register_error_handler(404, self.page_not_found) - - # Actions to control the bot - self.app.add_url_rule(f'{BASE_URI}/token/login', 'login', - view_func=self._token_login, methods=['POST']) - self.app.add_url_rule(f'{BASE_URI}/token/refresh', 'token_refresh', - view_func=self._token_refresh, methods=['POST']) - self.app.add_url_rule(f'{BASE_URI}/start', 'start', - view_func=self._start, methods=['POST']) - self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST']) - self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy', - view_func=self._stopbuy, methods=['POST']) - self.app.add_url_rule(f'{BASE_URI}/reload_config', 'reload_config', - view_func=self._reload_config, methods=['POST']) - # Info commands - self.app.add_url_rule(f'{BASE_URI}/balance', 'balance', - view_func=self._balance, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/locks', 'locks', view_func=self._locks, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/profit', 'profit', - view_func=self._profit, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/stats', 'stats', - view_func=self._stats, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/performance', 'performance', - view_func=self._performance, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/status', 'status', - view_func=self._status, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/version', 'version', - view_func=self._version, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/show_config', 'show_config', - view_func=self._show_config, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/ping', 'ping', - view_func=self._ping, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/trades', 'trades', - view_func=self._trades, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/trades/', 'trades_delete', - view_func=self._trades_delete, methods=['DELETE']) - - self.app.add_url_rule(f'{BASE_URI}/pair_candles', 'pair_candles', - view_func=self._analysed_candles, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/pair_history', 'pair_history', - view_func=self._analysed_history, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/plot_config', 'plot_config', - view_func=self._plot_config, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/strategies', 'strategies', - view_func=self._list_strategies, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/strategy/', 'strategy', - view_func=self._get_strategy, methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/available_pairs', 'pairs', - view_func=self._list_available_pairs, methods=['GET']) - - # Combined actions and infos - self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, - methods=['GET', 'POST']) - self.app.add_url_rule(f'{BASE_URI}/whitelist', 'whitelist', view_func=self._whitelist, - methods=['GET']) - self.app.add_url_rule(f'{BASE_URI}/forcebuy', 'forcebuy', - view_func=self._forcebuy, methods=['POST']) - self.app.add_url_rule(f'{BASE_URI}/forcesell', 'forcesell', view_func=self._forcesell, - methods=['POST']) - - @require_login - def page_not_found(self, error): - """ - Return "404 not found", 404. - """ - return jsonify({ - 'status': 'error', - 'reason': f"There's no API call for {request.base_url}.", - 'code': 404 - }), 404 - - @require_login - @rpc_catch_errors - def _token_login(self): - """ - Handler for /token/login - Returns a JWT token - """ - auth = request.authorization - if auth and self.check_auth(auth.username, auth.password): - keystuff = {'u': auth.username} - ret = { - 'access_token': create_access_token(identity=keystuff), - 'refresh_token': create_refresh_token(identity=keystuff), - } - return jsonify(ret) - - return jsonify({"error": "Unauthorized"}), 401 - - @jwt_refresh_token_required - @rpc_catch_errors - def _token_refresh(self): - """ - Handler for /token/refresh - Returns a JWT token based on a JWT refresh token - """ - current_user = get_jwt_identity() - new_token = create_access_token(identity=current_user, fresh=False) - - ret = {'access_token': new_token} - return jsonify(ret) - - @require_login - @rpc_catch_errors - def _start(self): - """ - Handler for /start. - Starts TradeThread in bot if stopped. - """ - msg = self._rpc._rpc_start() - return jsonify(msg) - - @require_login - @rpc_catch_errors - def _stop(self): - """ - Handler for /stop. - Stops TradeThread in bot if running - """ - msg = self._rpc._rpc_stop() - return jsonify(msg) - - @require_login - @rpc_catch_errors - def _stopbuy(self): - """ - Handler for /stopbuy. - Sets max_open_trades to 0 and gracefully sells all open trades - """ - msg = self._rpc._rpc_stopbuy() - return jsonify(msg) - - @rpc_catch_errors - def _ping(self): - """ - simple ping version - """ - return jsonify({"status": "pong"}) - - @require_login - @rpc_catch_errors - def _version(self): - """ - Prints the bot's version - """ - return jsonify({"version": __version__}) - - @require_login - @rpc_catch_errors - def _show_config(self): - """ - Prints the bot's version - """ - return jsonify(RPC._rpc_show_config(self._config, self._rpc._freqtrade.state)) - - @require_login - @rpc_catch_errors - def _reload_config(self): - """ - Handler for /reload_config. - Triggers a config file reload - """ - msg = self._rpc._rpc_reload_config() - return jsonify(msg) - - @require_login - @rpc_catch_errors - def _count(self): - """ - Handler for /count. - Returns the number of trades running - """ - msg = self._rpc._rpc_count() - return jsonify(msg) - - @require_login - @rpc_catch_errors - def _locks(self): - """ - Handler for /locks. - Returns the currently active locks. - """ - return jsonify(self._rpc._rpc_locks()) - - @require_login - @rpc_catch_errors - def _daily(self): - """ - Returns the last X days trading stats summary. - - :return: stats - """ - timescale = request.args.get('timescale', 7) - timescale = int(timescale) - - stats = self._rpc._rpc_daily_profit(timescale, - self._config['stake_currency'], - self._config.get('fiat_display_currency', '') - ) - - return jsonify(stats) - - @require_login - @rpc_catch_errors - def _get_logs(self): - """ - Returns latest logs - get: - param: - limit: Only get a certain number of records - """ - limit = int(request.args.get('limit', 0)) or None - return jsonify(RPC._rpc_get_logs(limit)) - - @require_login - @rpc_catch_errors - def _edge(self): - """ - Returns information related to Edge. - :return: edge stats - """ - stats = self._rpc._rpc_edge() - - return jsonify(stats) - - @require_login - @rpc_catch_errors - def _profit(self): - """ - Handler for /profit. - - Returns a cumulative profit statistics - :return: stats - """ - - stats = self._rpc._rpc_trade_statistics(self._config['stake_currency'], - self._config.get('fiat_display_currency') - ) - - return jsonify(stats) - - @require_login - @rpc_catch_errors - def _stats(self): - """ - Handler for /stats. - Returns a Object with "durations" and "sell_reasons" as keys. - """ - - stats = self._rpc._rpc_stats() - - return jsonify(stats) - - @require_login - @rpc_catch_errors - def _performance(self): - """ - Handler for /performance. - - Returns a cumulative performance statistics - :return: stats - """ - stats = self._rpc._rpc_performance() - - return jsonify(stats) - - @require_login - @rpc_catch_errors - def _status(self): - """ - Handler for /status. - - Returns the current status of the trades in json format - """ - try: - results = self._rpc._rpc_trade_status() - return jsonify(results) - except RPCException: - return jsonify([]) - - @require_login - @rpc_catch_errors - def _balance(self): - """ - Handler for /balance. - - Returns the current status of the trades in json format - """ - results = self._rpc._rpc_balance(self._config['stake_currency'], - self._config.get('fiat_display_currency', '')) - return jsonify(results) - - @require_login - @rpc_catch_errors - def _trades(self): - """ - Handler for /trades. - - Returns the X last trades in json format - """ - limit = int(request.args.get('limit', 0)) - results = self._rpc._rpc_trade_history(limit) - return jsonify(results) - - @require_login - @rpc_catch_errors - def _trades_delete(self, tradeid: int): - """ - Handler for DELETE /trades/ endpoint. - Removes the trade from the database (tries to cancel open orders first!) - get: - param: - tradeid: Numeric trade-id assigned to the trade. - """ - result = self._rpc._rpc_delete(tradeid) - return jsonify(result) - - @require_login - @rpc_catch_errors - def _whitelist(self): - """ - Handler for /whitelist. - """ - results = self._rpc._rpc_whitelist() - return jsonify(results) - - @require_login - @rpc_catch_errors - def _blacklist(self): - """ - Handler for /blacklist. - """ - add = request.json.get("blacklist", None) if request.method == 'POST' else None - results = self._rpc._rpc_blacklist(add) - return jsonify(results) - - @require_login - @rpc_catch_errors - def _forcebuy(self): - """ - Handler for /forcebuy. - """ - asset = request.json.get("pair") - price = request.json.get("price", None) - price = float(price) if price is not None else price - - trade = self._rpc._rpc_forcebuy(asset, price) - if trade: - return jsonify(trade.to_json()) - else: - return jsonify({"status": f"Error buying pair {asset}."}) - - @require_login - @rpc_catch_errors - def _forcesell(self): - """ - Handler for /forcesell. - """ - tradeid = request.json.get("tradeid") - results = self._rpc._rpc_forcesell(tradeid) - return jsonify(results) - - @require_login - @rpc_catch_errors - def _analysed_candles(self): - """ - Handler for /pair_candles. - Returns the dataframe the bot is using during live/dry operations. - Takes the following get arguments: - get: - parameters: - - pair: Pair - - timeframe: Timeframe to get data for (should be aligned to strategy.timeframe) - - limit: Limit return length to the latest X candles - """ - pair = request.args.get("pair") - timeframe = request.args.get("timeframe") - limit = request.args.get("limit", type=int) - if not pair or not timeframe: - return self.rest_error("Mandatory parameter missing.", 400) - - results = self._rpc._rpc_analysed_dataframe(pair, timeframe, limit) - return jsonify(results) - - @require_login - @rpc_catch_errors - def _analysed_history(self): - """ - Handler for /pair_history. - Returns the dataframe of a given timerange - Takes the following get arguments: - get: - parameters: - - pair: Pair - - timeframe: Timeframe to get data for (should be aligned to strategy.timeframe) - - strategy: Strategy to use - Must exist in configured strategy-path! - - timerange: timerange in the format YYYYMMDD-YYYYMMDD (YYYYMMDD- or (-YYYYMMDD)) - are als possible. If omitted uses all available data. - """ - pair = request.args.get("pair") - timeframe = request.args.get("timeframe") - timerange = request.args.get("timerange") - strategy = request.args.get("strategy") - - if not pair or not timeframe or not timerange or not strategy: - return self.rest_error("Mandatory parameter missing.", 400) - - config = deepcopy(self._config) - config.update({ - 'strategy': strategy, - }) - results = RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) - return jsonify(results) - - @require_login - @rpc_catch_errors - def _plot_config(self): - """ - Handler for /plot_config. - """ - return jsonify(self._rpc._rpc_plot_config()) - - @require_login - @rpc_catch_errors - def _list_strategies(self): - directory = Path(self._config.get( - 'strategy_path', self._config['user_data_dir'] / USERPATH_STRATEGIES)) - from freqtrade.resolvers.strategy_resolver import StrategyResolver - strategy_objs = StrategyResolver.search_all_objects(directory, False) - strategy_objs = sorted(strategy_objs, key=lambda x: x['name']) - - return jsonify({'strategies': [x['name'] for x in strategy_objs]}) - - @require_login - @rpc_catch_errors - def _get_strategy(self, strategy: str): - """ - Get a single strategy - get: - parameters: - - strategy: Only get this strategy - """ - config = deepcopy(self._config) - from freqtrade.resolvers.strategy_resolver import StrategyResolver - try: - strategy_obj = StrategyResolver._load_strategy(strategy, config, - extra_dir=config.get('strategy_path')) - except OperationalException: - return self.rest_error("Strategy not found.", 404) - - return jsonify({ - 'strategy': strategy_obj.get_strategy_name(), - 'code': strategy_obj.__source__, - }) - - @require_login - @rpc_catch_errors - def _list_available_pairs(self): - """ - Handler for /available_pairs. - Returns an object, with pairs, available pair length and pair_interval combinations - Takes the following get arguments: - get: - parameters: - - stake_currency: Filter on this stake currency - - timeframe: Timeframe to get data for Filter elements to this timeframe - """ - timeframe = request.args.get("timeframe") - stake_currency = request.args.get("stake_currency") - - from freqtrade.data.history import get_datahandler - dh = get_datahandler(self._config['datadir'], self._config.get('dataformat_ohlcv', None)) - - pair_interval = dh.ohlcv_get_available_data(self._config['datadir']) - - if timeframe: - pair_interval = [pair for pair in pair_interval if pair[1] == timeframe] - if stake_currency: - pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)] - pair_interval = sorted(pair_interval, key=lambda x: x[0]) - - pairs = list({x[0] for x in pair_interval}) - - result = { - 'length': len(pairs), - 'pairs': pairs, - 'pair_interval': pair_interval, - } - return jsonify(result) diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index b3e6eb0dc..793e581ce 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -1,6 +1,6 @@ import logging from ipaddress import IPv4Address -from typing import Any, Dict, Optional +from typing import Any, Dict import uvicorn from fastapi import Depends, FastAPI diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8017293b4..c49f6a1d1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -5,23 +5,22 @@ Unit test file for rpc/api_server.py from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock -from fastapi.applications import FastAPI import pytest -from fastapi.testclient import TestClient from fastapi import FastAPI +from fastapi.testclient import TestClient from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC -from freqtrade.rpc.api_server111 import BASE_URI # , ApiServer from freqtrade.rpc.api_server2 import ApiServer from freqtrade.state import RunMode, State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal +BASE_URI = "/api/v1" _TEST_USER = "FreqTrader" _TEST_PASS = "SuperSecurePassword1!" From 29ce323649ec354c15c4aa61bcbeb99c38b0f4a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Dec 2020 14:21:05 +0100 Subject: [PATCH 104/563] Fix wrong hyperoptlosstest --- tests/optimize/test_hyperoptloss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index 63012ee48..f7910e6d6 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -20,7 +20,7 @@ def test_hyperoptlossresolver(mocker, default_conf) -> None: hl = ShortTradeDurHyperOptLoss mocker.patch( 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', - MagicMock(return_value=hl) + MagicMock(return_value=hl()) ) default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) x = HyperOptLossResolver.load_hyperoptloss(default_conf) From 790f8336531c2ff9e22c556e6658659deec7a2eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Dec 2020 14:57:01 +0100 Subject: [PATCH 105/563] Some more tests around api_auth --- freqtrade/rpc/api_server2/api_auth.py | 2 +- freqtrade/rpc/api_server2/uvicorn_threaded.py | 5 -- tests/rpc/test_rpc_apiserver.py | 57 +++++++++++++++++-- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_auth.py b/freqtrade/rpc/api_server2/api_auth.py index 599f6b53c..595107acb 100644 --- a/freqtrade/rpc/api_server2/api_auth.py +++ b/freqtrade/rpc/api_server2/api_auth.py @@ -73,7 +73,7 @@ def http_basic_or_jwt_token(form_data: HTTPBasicCredentials = Depends(httpbasic) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", + detail="Unauthorized", ) diff --git a/freqtrade/rpc/api_server2/uvicorn_threaded.py b/freqtrade/rpc/api_server2/uvicorn_threaded.py index ce7089bed..1554a8e52 100644 --- a/freqtrade/rpc/api_server2/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server2/uvicorn_threaded.py @@ -19,13 +19,8 @@ class UvicornServer(uvicorn.Server): def run_in_thread(self): self.thread = threading.Thread(target=self.run) self.thread.start() - # try: while not self.started: time.sleep(1e-3) - # yield - # finally: - # self.should_exit = True - # thread.join() def cleanup(self): self.should_exit = True diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c49f6a1d1..ee8976741 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -3,6 +3,12 @@ Unit test file for rpc/api_server.py """ from datetime import datetime, timedelta, timezone + +import uvicorn +from freqtrade.rpc.api_server2.uvicorn_threaded import UvicornServer + +from fastapi.exceptions import HTTPException +from freqtrade.rpc.api_server2.api_auth import create_token, get_user_from_token from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock @@ -83,6 +89,26 @@ def test_api_not_found(botclient): assert rc.json() == {"detail": "Not Found"} +def test_api_auth(): + with pytest.raises(ValueError): + create_token({'sub': 'Freqtrade'}, token_type="NotATokenType") + + token = create_token({'sub': 'Freqtrade'}, ) + assert isinstance(token, bytes) + + u = get_user_from_token(token) + assert u == 'Freqtrade' + with pytest.raises(HTTPException): + get_user_from_token(token, token_type='refresh') + # Create invalid token + token = create_token({'sub`': 'Freqtrade'}, ) + with pytest.raises(HTTPException): + get_user_from_token(token) + + with pytest.raises(HTTPException): + get_user_from_token(b'not_a_token') + + def test_api_unauthorized(botclient): ftbot, client = botclient rc = client.get(f"{BASE_URI}/ping") @@ -92,31 +118,36 @@ def test_api_unauthorized(botclient): # Don't send user/pass information rc = client.get(f"{BASE_URI}/version") assert_response(rc, 401, needs_cors=False) - assert rc.json() == {'error': 'Unauthorized'} + assert rc.json() == {'detail': 'Unauthorized'} # Change only username ftbot.config['api_server']['username'] = 'Ftrader' rc = client_get(client, f"{BASE_URI}/version") assert_response(rc, 401) - assert rc.json() == {'error': 'Unauthorized'} + assert rc.json() == {'detail': 'Unauthorized'} # Change only password ftbot.config['api_server']['username'] = _TEST_USER ftbot.config['api_server']['password'] = 'WrongPassword' rc = client_get(client, f"{BASE_URI}/version") assert_response(rc, 401) - assert rc.json() == {'error': 'Unauthorized'} + assert rc.json() == {'detail': 'Unauthorized'} ftbot.config['api_server']['username'] = 'Ftrader' ftbot.config['api_server']['password'] = 'WrongPassword' rc = client_get(client, f"{BASE_URI}/version") assert_response(rc, 401) - assert rc.json() == {'error': 'Unauthorized'} + assert rc.json() == {'detail': 'Unauthorized'} def test_api_token_login(botclient): ftbot, client = botclient + rc = client.post(f"{BASE_URI}/token/login", + data=None, + headers={'Authorization': _basic_auth_str('WRONG_USER', 'WRONG_PASS'), + 'Origin': 'http://example.com'}) + assert_response(rc, 401) rc = client_post(client, f"{BASE_URI}/token/login") assert_response(rc) assert 'access_token' in rc.json() @@ -183,6 +214,24 @@ def test_api__init__(default_conf, mocker): assert apiserver._config == default_conf +def test_api_UvicornServer(default_conf, mocker): + thread_mock = mocker.patch('freqtrade.rpc.api_server2.uvicorn_threaded.threading.Thread') + s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) + assert thread_mock.call_count == 0 + + s.install_signal_handlers() + # Original implementation starts a thread - make sure that's not the case + assert thread_mock.call_count == 0 + + # Fake started to avoid sleeping forever + s.started = True + s.run_in_thread() + assert thread_mock.call_count == 1 + + s.cleanup() + assert s.should_exit is True + + def test_api_run(default_conf, mocker, caplog): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", From 1717121f10d10b9d123c5c2ba95e3a9828ec4d1b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Dec 2020 15:24:49 +0100 Subject: [PATCH 106/563] Properly use JWT secret key --- freqtrade/rpc/api_server2/api_auth.py | 43 ++++++++++++++------------ freqtrade/rpc/api_server2/deps.py | 4 +++ freqtrade/rpc/api_server2/webserver.py | 5 +++ tests/rpc/test_rpc_apiserver.py | 18 ++++++----- 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_auth.py b/freqtrade/rpc/api_server2/api_auth.py index 595107acb..00ae60ed2 100644 --- a/freqtrade/rpc/api_server2/api_auth.py +++ b/freqtrade/rpc/api_server2/api_auth.py @@ -8,34 +8,32 @@ from fastapi.security.http import HTTPBasic, HTTPBasicCredentials from freqtrade.rpc.api_server2.api_models import AccessAndRefreshToken, AccessToken -from .deps import get_config +from .deps import get_api_config -SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 router_login = APIRouter() -def verify_auth(config, username: str, password: str): +def verify_auth(api_config, username: str, password: str): """Verify username/password""" - return (secrets.compare_digest(username, config['api_server'].get('username')) and - secrets.compare_digest(password, config['api_server'].get('password'))) + return (secrets.compare_digest(username, api_config.get('username')) and + secrets.compare_digest(password, api_config.get('password'))) httpbasic = HTTPBasic(auto_error=False) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) -def get_user_from_token(token, token_type: str = "access"): +def get_user_from_token(token, secret_key: str, token_type: str = "access"): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + payload = jwt.decode(token, secret_key, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise credentials_exception @@ -47,7 +45,7 @@ def get_user_from_token(token, token_type: str = "access"): return username -def create_token(data: dict, token_type: str = "access") -> str: +def create_token(data: dict, secret_key: str, token_type: str = "access") -> str: to_encode = data.copy() if token_type == "access": expire = datetime.utcnow() + timedelta(minutes=15) @@ -60,15 +58,16 @@ def create_token(data: dict, token_type: str = "access") -> str: "iat": datetime.utcnow(), "type": token_type, }) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + encoded_jwt = jwt.encode(to_encode, secret_key, algorithm=ALGORITHM) return encoded_jwt def http_basic_or_jwt_token(form_data: HTTPBasicCredentials = Depends(httpbasic), - token: str = Depends(oauth2_scheme), config=Depends(get_config)): + token: str = Depends(oauth2_scheme), + api_config=Depends(get_api_config)): if token: - return get_user_from_token(token) - elif form_data and verify_auth(config, form_data.username, form_data.password): + return get_user_from_token(token, api_config.get('jwt_secret_key', 'super-secret')) + elif form_data and verify_auth(api_config, form_data.username, form_data.password): return form_data.username raise HTTPException( @@ -78,12 +77,14 @@ def http_basic_or_jwt_token(form_data: HTTPBasicCredentials = Depends(httpbasic) @router_login.post('/token/login', response_model=AccessAndRefreshToken) -def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()), config=Depends(get_config)): +def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()), + api_config=Depends(get_api_config)): - if verify_auth(config, form_data.username, form_data.password): + if verify_auth(api_config, form_data.username, form_data.password): token_data = {'sub': form_data.username} - access_token = create_token(token_data) - refresh_token = create_token(token_data, token_type="refresh") + access_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret')) + refresh_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'), + token_type="refresh") return { "access_token": access_token, "refresh_token": refresh_token, @@ -97,9 +98,11 @@ def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()), config=D @router_login.post('/token/refresh', response_model=AccessToken) -def token_refresh(token: str = Depends(oauth2_scheme)): +def token_refresh(token: str = Depends(oauth2_scheme), api_config=Depends(get_api_config)): # Refresh token - u = get_user_from_token(token, 'refresh') + u = get_user_from_token(token, api_config.get( + 'jwt_secret_key', 'super-secret'), 'refresh') token_data = {'sub': u} - access_token = create_token(token_data, token_type="access") + access_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'), + token_type="access") return {'access_token': access_token} diff --git a/freqtrade/rpc/api_server2/deps.py b/freqtrade/rpc/api_server2/deps.py index 60cc6b8fb..691759012 100644 --- a/freqtrade/rpc/api_server2/deps.py +++ b/freqtrade/rpc/api_server2/deps.py @@ -7,3 +7,7 @@ def get_rpc(): def get_config(): return ApiServer._config + + +def get_api_config(): + return ApiServer._config['api_server'] diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index 793e581ce..3956e52db 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -86,6 +86,11 @@ class ApiServer(RPCHandler): logger.warning("SECURITY WARNING - No password for local REST Server defined. " "Please make sure that this is intentional!") + if (self._config['api_server'].get('jwt_secret_key', 'super-secret') + in ('super-secret, somethingrandom')): + logger.warning("SECURITY WARNING - `jwt_secret_key` seems to be default." + "Others may be able to log into your bot.") + logger.info('Starting Local Rest Server.') uvconfig = uvicorn.Config(self.app, port=rest_port, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index ee8976741..95789a85f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -23,7 +23,7 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC from freqtrade.rpc.api_server2 import ApiServer from freqtrade.state import RunMode, State -from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal +from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, patch_get_signal BASE_URI = "/api/v1" @@ -91,22 +91,22 @@ def test_api_not_found(botclient): def test_api_auth(): with pytest.raises(ValueError): - create_token({'sub': 'Freqtrade'}, token_type="NotATokenType") + create_token({'sub': 'Freqtrade'}, 'secret1234', token_type="NotATokenType") - token = create_token({'sub': 'Freqtrade'}, ) + token = create_token({'sub': 'Freqtrade'}, 'secret1234') assert isinstance(token, bytes) - u = get_user_from_token(token) + u = get_user_from_token(token, 'secret1234') assert u == 'Freqtrade' with pytest.raises(HTTPException): - get_user_from_token(token, token_type='refresh') + get_user_from_token(token, 'secret1234', token_type='refresh') # Create invalid token - token = create_token({'sub`': 'Freqtrade'}, ) + token = create_token({'sub`': 'Freqtrade'}, 'secret1234') with pytest.raises(HTTPException): - get_user_from_token(token) + get_user_from_token(token, 'secret1234') with pytest.raises(HTTPException): - get_user_from_token(b'not_a_token') + get_user_from_token(b'not_a_token', 'secret1234') def test_api_unauthorized(botclient): @@ -279,6 +279,8 @@ def test_api_run(default_conf, mocker, caplog): "e.g 127.0.0.1 in config.json", caplog) assert log_has("SECURITY WARNING - No password for local REST Server defined. " "Please make sure that this is intentional!", caplog) + assert log_has_re("SECURITY WARNING - `jwt_secret_key` seems to be default.*", caplog) + # Test crashing flask caplog.clear() From 68d148e72d1b5da8951d976c6ce96e355ac0165d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Dec 2020 15:54:05 +0100 Subject: [PATCH 107/563] Allow configuration of openAPI interface --- config_full.json.example | 1 + docs/rest-api.md | 6 ++++++ freqtrade/rpc/api_server2/webserver.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index e69e52469..ef9f45363 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -165,6 +165,7 @@ "listen_ip_address": "127.0.0.1", "listen_port": 8080, "verbosity": "info", + "enable_openapi": false, "jwt_secret_key": "somethingrandom", "CORS_origins": [], "username": "freqtrader", diff --git a/docs/rest-api.md b/docs/rest-api.md index 9bb35ce91..279373c50 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -12,6 +12,7 @@ Sample configuration: "listen_ip_address": "127.0.0.1", "listen_port": 8080, "verbosity": "info", + "enable_openapi": false, "jwt_secret_key": "somethingrandom", "CORS_origins": [], "username": "Freqtrader", @@ -263,6 +264,11 @@ whitelist ``` +## OpenAPI interface + +To enable the builtin openAPI interface, specify `"enable_openapi": true` in the api_server configuration. +This will enable the Swagger UI at the `/docs` endpoint. By default, that's running at http://localhost:8080/docs/ - but it'll depend on your settings. + ## Advanced API usage using JWT tokens !!! Note diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index 3956e52db..c5cc30156 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -26,8 +26,13 @@ class ApiServer(RPCHandler): ApiServer._rpc = rpc ApiServer._config = config + api_config = self._config['api_server'] - self.app = FastAPI(title="Freqtrade API") + self.app = FastAPI(title="Freqtrade API", + openapi_url='openapi.json' if api_config.get( + 'enable_openapi') else None, + redoc_url=None, + ) self.configure_app(self.app, self._config) self.start_api() @@ -92,10 +97,11 @@ class ApiServer(RPCHandler): "Others may be able to log into your bot.") logger.info('Starting Local Rest Server.') + verbosity = self._config['api_server'].get('verbosity', 'info') uvconfig = uvicorn.Config(self.app, port=rest_port, host=rest_ip, - access_log=True) + access_log=True if verbosity != 'error' else False) try: self._server = UvicornServer(uvconfig) self._server.run_in_thread() From 346542e5cddc05892521c1fc7d01223126efc5f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Dec 2020 17:23:28 +0100 Subject: [PATCH 108/563] Remove flask dependency --- freqtrade/rpc/api_server2/api_auth.py | 1 - freqtrade/rpc/api_server2/webserver.py | 3 ++- requirements.txt | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_auth.py b/freqtrade/rpc/api_server2/api_auth.py index 00ae60ed2..6f5d051d3 100644 --- a/freqtrade/rpc/api_server2/api_auth.py +++ b/freqtrade/rpc/api_server2/api_auth.py @@ -93,7 +93,6 @@ def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()), raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", - headers={"WWW-Authenticate": "Basic"}, ) diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server2/webserver.py index c5cc30156..f603ab160 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server2/webserver.py @@ -101,7 +101,8 @@ class ApiServer(RPCHandler): uvconfig = uvicorn.Config(self.app, port=rest_port, host=rest_ip, - access_log=True if verbosity != 'error' else False) + access_log=True if verbosity != 'error' else False, + ) try: self._server = UvicornServer(uvconfig) self._server.run_in_thread() diff --git a/requirements.txt b/requirements.txt index 4b439079b..779cc3771 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,11 +27,6 @@ python-rapidjson==1.0 # Notify systemd sdnotify==0.3.2 -# Api server -flask==1.1.2 -flask-jwt-extended==3.25.0 -flask-cors==3.0.9 - # API Server fastapi==0.63.0 uvicorn==0.13.2 From eb20f6e7d06b8de23c0a22d31e11bd60ace93c94 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Dec 2020 10:22:28 +0100 Subject: [PATCH 109/563] Align auth token to flask version to prevent user-logout --- freqtrade/rpc/api_server2/api_auth.py | 6 +++--- freqtrade/rpc/rpc_manager.py | 2 -- tests/rpc/test_rpc_apiserver.py | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/api_server2/api_auth.py b/freqtrade/rpc/api_server2/api_auth.py index 6f5d051d3..a02accb18 100644 --- a/freqtrade/rpc/api_server2/api_auth.py +++ b/freqtrade/rpc/api_server2/api_auth.py @@ -34,7 +34,7 @@ def get_user_from_token(token, secret_key: str, token_type: str = "access"): ) try: payload = jwt.decode(token, secret_key, algorithms=[ALGORITHM]) - username: str = payload.get("sub") + username: str = payload.get("identity", {}).get('u') if username is None: raise credentials_exception if payload.get("type") != token_type: @@ -81,7 +81,7 @@ def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()), api_config=Depends(get_api_config)): if verify_auth(api_config, form_data.username, form_data.password): - token_data = {'sub': form_data.username} + token_data = {'identity': {'u': form_data.username}} access_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret')) refresh_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'), token_type="refresh") @@ -101,7 +101,7 @@ def token_refresh(token: str = Depends(oauth2_scheme), api_config=Depends(get_ap # Refresh token u = get_user_from_token(token, api_config.get( 'jwt_secret_key', 'super-secret'), 'refresh') - token_data = {'sub': u} + token_data = {'identity': {'u': u}} access_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'), token_type="access") return {'access_token': access_token} diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 2afd39eda..369dfa5c9 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -34,8 +34,6 @@ class RPCManager: # Enable local rest api server for cmd line control if config.get('api_server', {}).get('enabled', False): logger.info('Enabling rpc.api_server') - # from freqtrade.rpc.api_server import ApiServer - # TODO: Remove the above import from freqtrade.rpc.api_server2 import ApiServer self.registered_modules.append(ApiServer(self._rpc, config)) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 95789a85f..470032357 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -91,9 +91,9 @@ def test_api_not_found(botclient): def test_api_auth(): with pytest.raises(ValueError): - create_token({'sub': 'Freqtrade'}, 'secret1234', token_type="NotATokenType") + create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234', token_type="NotATokenType") - token = create_token({'sub': 'Freqtrade'}, 'secret1234') + token = create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234') assert isinstance(token, bytes) u = get_user_from_token(token, 'secret1234') @@ -101,7 +101,7 @@ def test_api_auth(): with pytest.raises(HTTPException): get_user_from_token(token, 'secret1234', token_type='refresh') # Create invalid token - token = create_token({'sub`': 'Freqtrade'}, 'secret1234') + token = create_token({'identity': {'u1': 'Freqrade'}}, 'secret1234') with pytest.raises(HTTPException): get_user_from_token(token, 'secret1234') From b2ab553a31d99338fa5add94bb4be2b53997fc18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Dec 2020 11:01:50 +0100 Subject: [PATCH 110/563] Rename api_server2 module to apiserver --- .../{api_server2 => api_server}/__init__.py | 0 .../{api_server2 => api_server}/api_auth.py | 7 ++--- .../{api_server2 => api_server}/api_models.py | 2 +- .../rpc/{api_server2 => api_server}/api_v1.py | 6 ++-- .../rpc/{api_server2 => api_server}/deps.py | 0 .../uvicorn_threaded.py | 0 .../{api_server2 => api_server}/webserver.py | 2 +- freqtrade/rpc/rpc_manager.py | 2 +- tests/rpc/test_rpc_apiserver.py | 29 +++++++++---------- tests/rpc/test_rpc_manager.py | 4 +-- 10 files changed, 25 insertions(+), 27 deletions(-) rename freqtrade/rpc/{api_server2 => api_server}/__init__.py (100%) rename freqtrade/rpc/{api_server2 => api_server}/api_auth.py (95%) rename freqtrade/rpc/{api_server2 => api_server}/api_models.py (98%) rename freqtrade/rpc/{api_server2 => api_server}/api_v1.py (97%) rename freqtrade/rpc/{api_server2 => api_server}/deps.py (100%) rename freqtrade/rpc/{api_server2 => api_server}/uvicorn_threaded.py (100%) rename freqtrade/rpc/{api_server2 => api_server}/webserver.py (99%) diff --git a/freqtrade/rpc/api_server2/__init__.py b/freqtrade/rpc/api_server/__init__.py similarity index 100% rename from freqtrade/rpc/api_server2/__init__.py rename to freqtrade/rpc/api_server/__init__.py diff --git a/freqtrade/rpc/api_server2/api_auth.py b/freqtrade/rpc/api_server/api_auth.py similarity index 95% rename from freqtrade/rpc/api_server2/api_auth.py rename to freqtrade/rpc/api_server/api_auth.py index a02accb18..8d1316906 100644 --- a/freqtrade/rpc/api_server2/api_auth.py +++ b/freqtrade/rpc/api_server/api_auth.py @@ -6,9 +6,8 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from fastapi.security.http import HTTPBasic, HTTPBasicCredentials -from freqtrade.rpc.api_server2.api_models import AccessAndRefreshToken, AccessToken - -from .deps import get_api_config +from freqtrade.rpc.api_server.api_models import AccessAndRefreshToken, AccessToken +from freqtrade.rpc.api_server.deps import get_api_config ALGORITHM = "HS256" @@ -45,7 +44,7 @@ def get_user_from_token(token, secret_key: str, token_type: str = "access"): return username -def create_token(data: dict, secret_key: str, token_type: str = "access") -> str: +def create_token(data: dict, secret_key: str, token_type: str = "access") -> bytes: to_encode = data.copy() if token_type == "access": expire = datetime.utcnow() + timedelta(minutes=15) diff --git a/freqtrade/rpc/api_server2/api_models.py b/freqtrade/rpc/api_server/api_models.py similarity index 98% rename from freqtrade/rpc/api_server2/api_models.py rename to freqtrade/rpc/api_server/api_models.py index c9e4ee5cc..a8b03eac5 100644 --- a/freqtrade/rpc/api_server2/api_models.py +++ b/freqtrade/rpc/api_server/api_models.py @@ -167,7 +167,7 @@ class DeleteTrade(BaseModel): class PlotConfig(BaseModel): - main_plot: Optional[Dict[str, Any]] + main_plot: Dict[str, Any] subplots: Optional[Dict[str, Any]] diff --git a/freqtrade/rpc/api_server2/api_v1.py b/freqtrade/rpc/api_server/api_v1.py similarity index 97% rename from freqtrade/rpc/api_server2/api_v1.py rename to freqtrade/rpc/api_server/api_v1.py index 21c525850..af9592a7b 100644 --- a/freqtrade/rpc/api_server2/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -83,7 +83,7 @@ def status(rpc: RPC = Depends(get_rpc)): # TODO: Missing response model @router.get('/trades', tags=['info', 'trading']) -def trades(limit: Optional[int] = 0, rpc: RPC = Depends(get_rpc)): +def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit) @@ -180,8 +180,8 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) -@router.get('/plot_config', response_model=Union[Dict, PlotConfig], tags=['candle data']) -def plot_config(rpc=Depends(get_rpc)): +@router.get('/plot_config', response_model=Union[PlotConfig, Dict], tags=['candle data']) +def plot_config(rpc: RPC = Depends(get_rpc)): return rpc._rpc_plot_config() diff --git a/freqtrade/rpc/api_server2/deps.py b/freqtrade/rpc/api_server/deps.py similarity index 100% rename from freqtrade/rpc/api_server2/deps.py rename to freqtrade/rpc/api_server/deps.py diff --git a/freqtrade/rpc/api_server2/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py similarity index 100% rename from freqtrade/rpc/api_server2/uvicorn_threaded.py rename to freqtrade/rpc/api_server/uvicorn_threaded.py diff --git a/freqtrade/rpc/api_server2/webserver.py b/freqtrade/rpc/api_server/webserver.py similarity index 99% rename from freqtrade/rpc/api_server2/webserver.py rename to freqtrade/rpc/api_server/webserver.py index f603ab160..caddcba84 100644 --- a/freqtrade/rpc/api_server2/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) class ApiServer(RPCHandler): - _rpc: RPC = None + _rpc: RPC _config: Dict[str, Any] = {} def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 369dfa5c9..7977d68de 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -34,7 +34,7 @@ class RPCManager: # Enable local rest api server for cmd line control if config.get('api_server', {}).get('enabled', False): logger.info('Enabling rpc.api_server') - from freqtrade.rpc.api_server2 import ApiServer + from freqtrade.rpc.api_server import ApiServer self.registered_modules.append(ApiServer(self._rpc, config)) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 470032357..8cad9d808 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -3,17 +3,13 @@ Unit test file for rpc/api_server.py """ from datetime import datetime, timedelta, timezone - -import uvicorn -from freqtrade.rpc.api_server2.uvicorn_threaded import UvicornServer - -from fastapi.exceptions import HTTPException -from freqtrade.rpc.api_server2.api_auth import create_token, get_user_from_token from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock import pytest +import uvicorn from fastapi import FastAPI +from fastapi.exceptions import HTTPException from fastapi.testclient import TestClient from requests.auth import _basic_auth_str @@ -21,9 +17,12 @@ from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC -from freqtrade.rpc.api_server2 import ApiServer +from freqtrade.rpc.api_server import ApiServer +from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token +from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.state import RunMode, State -from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, patch_get_signal +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, + patch_get_signal) BASE_URI = "/api/v1" @@ -46,7 +45,7 @@ def botclient(default_conf, mocker): ftbot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(ftbot) - mocker.patch('freqtrade.rpc.api_server2.ApiServer.start_api', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api', MagicMock()) apiserver = ApiServer(rpc, default_conf) yield ftbot, TestClient(apiserver.app) # Cleanup ... ? @@ -209,13 +208,13 @@ def test_api__init__(default_conf, mocker): "password": "testPass", }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) - mocker.patch('freqtrade.rpc.api_server2.webserver.ApiServer.start_api', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.webserver.ApiServer.start_api', MagicMock()) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) assert apiserver._config == default_conf def test_api_UvicornServer(default_conf, mocker): - thread_mock = mocker.patch('freqtrade.rpc.api_server2.uvicorn_threaded.threading.Thread') + thread_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.threading.Thread') s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) assert thread_mock.call_count == 0 @@ -242,7 +241,7 @@ def test_api_run(default_conf, mocker, caplog): mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) server_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server2.webserver.UvicornServer', server_mock) + mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer', server_mock) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) @@ -281,10 +280,10 @@ def test_api_run(default_conf, mocker, caplog): "Please make sure that this is intentional!", caplog) assert log_has_re("SECURITY WARNING - `jwt_secret_key` seems to be default.*", caplog) - # Test crashing flask caplog.clear() - mocker.patch('freqtrade.rpc.api_server2.webserver.UvicornServer', MagicMock(side_effect=Exception)) + mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer', + MagicMock(side_effect=Exception)) apiserver.start_api() assert log_has("Api server failed to start.", caplog) @@ -300,7 +299,7 @@ def test_api_cleanup(default_conf, mocker, caplog): server_mock = MagicMock() server_mock.cleanup = MagicMock() - mocker.patch('freqtrade.rpc.api_server2.webserver.UvicornServer', server_mock) + mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer', server_mock) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index e63d629b8..3068e9764 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -160,7 +160,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) run_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server2.ApiServer.start_api', run_mock) + mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api', run_mock) default_conf['telegram']['enabled'] = False rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) @@ -172,7 +172,7 @@ def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) run_mock = MagicMock() - mocker.patch('freqtrade.rpc.api_server2.ApiServer.start_api', run_mock) + mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api', run_mock) default_conf["telegram"]["enabled"] = False default_conf["api_server"] = {"enabled": True, From 718f2b24d2d8f70c1cd1ac9b92282501faed2032 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Jan 2021 19:13:32 +0100 Subject: [PATCH 111/563] Don't use relative imports --- freqtrade/rpc/api_server/api_v1.py | 15 ++++++++------- freqtrade/rpc/api_server/webserver.py | 9 ++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index af9592a7b..3ab378d15 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -10,15 +10,16 @@ from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.data.history import get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC +from freqtrade.rpc.api_server.api_models import (AvailablePairs, Balances, BlacklistPayload, + BlacklistResponse, Count, Daily, DeleteTrade, + ForceBuyPayload, ForceSellPayload, Locks, Logs, + PairHistory, PerformanceEntry, Ping, PlotConfig, + Profit, ResultMsg, Stats, StatusMsg, + StrategyListResponse, StrategyResponse, Version, + WhitelistResponse) +from freqtrade.rpc.api_server.deps import get_config, get_rpc from freqtrade.rpc.rpc import RPCException -from .api_models import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, - Daily, DeleteTrade, ForceBuyPayload, ForceSellPayload, Locks, Logs, - PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, Stats, - StatusMsg, StrategyListResponse, StrategyResponse, Version, - WhitelistResponse) -from .deps import get_config, get_rpc - # Public API, requires no auth. router_public = APIRouter() diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index caddcba84..ac7a35a9e 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -7,10 +7,9 @@ from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware from starlette.responses import JSONResponse +from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler -from .uvicorn_threaded import UvicornServer - logger = logging.getLogger(__name__) @@ -54,9 +53,9 @@ class ApiServer(RPCHandler): ) def configure_app(self, app: FastAPI, config): - from .api_auth import http_basic_or_jwt_token, router_login - from .api_v1 import router as api_v1 - from .api_v1 import router_public as api_v1_public + from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login + from freqtrade.rpc.api_server.api_v1 import router as api_v1 + from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public app.include_router(api_v1_public, prefix="/api/v1") app.include_router(api_v1, prefix="/api/v1", From 29f4dd1dcd61f7bc6471ab83a262d9b36b1a8167 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Jan 2021 19:36:03 +0100 Subject: [PATCH 112/563] Enhance some response models --- freqtrade/rpc/api_server/api_models.py | 54 ++++++++++++++++++++++++++ freqtrade/rpc/api_server/api_v1.py | 10 ++--- tests/rpc/test_rpc_apiserver.py | 9 +++-- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/api_server/api_models.py b/freqtrade/rpc/api_server/api_models.py index a8b03eac5..f4e62acdc 100644 --- a/freqtrade/rpc/api_server/api_models.py +++ b/freqtrade/rpc/api_server/api_models.py @@ -112,6 +112,60 @@ class Daily(BaseModel): stake_currency: str +class TradeSchema(BaseModel): + trade_id: str + pair: str + is_open: bool + exchange: str + amount: float + amount_requested: float + stake_amount: float + strategy: str + timeframe: str + fee_open: Optional[float] + fee_open_cost: Optional[float] + fee_open_currency: Optional[str] + fee_close: Optional[float] + fee_close_cost: Optional[float] + fee_close_currency: Optional[str] + open_date_hum: str + open_date: str + open_timestamp: int + open_rate: float + open_rate_requested: Optional[float] + open_trade_value: float + close_date_hum: Optional[str] + close_date: Optional[str] + close_timestamp: Optional[int] + close_rate: Optional[float] + close_rate_requested: Optional[float] + close_profit: Optional[float] + close_profit_pct: Optional[float] + close_profit_abs: Optional[float] + profit_ratio: Optional[float] + profit_pct: Optional[float] + profit_abs: Optional[float] + sell_reason: Optional[str] + sell_order_status: Optional[str] + stop_loss_abs: Optional[float] + stop_loss_ratio: Optional[float] + stop_loss_pct: Optional[float] + stoploss_order_id: Optional[str] + stoploss_last_update: Optional[str] + stoploss_last_update_timestamp: Optional[int] + initial_stop_loss_abs: Optional[float] + initial_stop_loss_ratio: Optional[float] + initial_stop_loss_pct: Optional[float] + min_rate: Optional[float] + max_rate: Optional[float] + open_order_id: Optional[str] + + +class TradeResponse(BaseModel): + trades: List[TradeSchema] + trades_count: int + + class LockModel(BaseModel): active: bool lock_end_time: str diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 3ab378d15..55bb9320c 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -15,7 +15,7 @@ from freqtrade.rpc.api_server.api_models import (AvailablePairs, Balances, Black ForceBuyPayload, ForceSellPayload, Locks, Logs, PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, Stats, StatusMsg, - StrategyListResponse, StrategyResponse, Version, + StrategyListResponse, StrategyResponse, TradeResponse, TradeSchema, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc from freqtrade.rpc.rpc import RPCException @@ -74,7 +74,7 @@ def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_co # TODO: Missing response model -@router.get('/status', tags=['info']) +@router.get('/status', response_model=List[TradeSchema], tags=['info']) def status(rpc: RPC = Depends(get_rpc)): try: return rpc._rpc_trade_status() @@ -82,8 +82,7 @@ def status(rpc: RPC = Depends(get_rpc)): return [] -# TODO: Missing response model -@router.get('/trades', tags=['info', 'trading']) +@router.get('/trades', response_model=TradeResponse, tags=['info', 'trading']) def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit) @@ -105,8 +104,7 @@ def show_config(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): return RPC._rpc_show_config(config, rpc._freqtrade.state) -# TODO: Missing response model -@router.post('/forcebuy', tags=['trading']) +@router.post('/forcebuy', response_model=Union[TradeSchema, StatusMsg], tags=['trading']) def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): trade = rpc._rpc_forcebuy(payload.pair, payload.price) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8cad9d808..5e972c694 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -853,6 +853,9 @@ def test_api_forcebuy(botclient, mocker, fee): fee_close=fee.return_value, fee_open=fee.return_value, close_rate=0.265441, + id='22', + timeframe="5m", + strategy="DefaultStrategy" )) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) @@ -862,7 +865,7 @@ def test_api_forcebuy(botclient, mocker, fee): assert rc.json() == { 'amount': 1, 'amount_requested': 1, - 'trade_id': None, + 'trade_id': '22', 'close_date': None, 'close_date_hum': None, 'close_timestamp': None, @@ -903,8 +906,8 @@ def test_api_forcebuy(botclient, mocker, fee): 'open_trade_value': 0.24605460, 'sell_reason': None, 'sell_order_status': None, - 'strategy': None, - 'timeframe': None, + 'strategy': 'DefaultStrategy', + 'timeframe': '5m', 'exchange': 'bittrex', } From 84ced92002f38f29a4e55de1088c85e08a94b4ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Jan 2021 19:38:28 +0100 Subject: [PATCH 113/563] Fix mock-tests missing some fields --- freqtrade/rpc/api_server/api_models.py | 18 ++++++++++++++++-- freqtrade/rpc/api_server/api_v1.py | 10 +++++----- tests/conftest_trades.py | 6 ++++++ tests/data/test_btanalysis.py | 2 +- tests/rpc/test_rpc_apiserver.py | 8 ++++---- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server/api_models.py b/freqtrade/rpc/api_server/api_models.py index f4e62acdc..8c2a25ceb 100644 --- a/freqtrade/rpc/api_server/api_models.py +++ b/freqtrade/rpc/api_server/api_models.py @@ -113,7 +113,7 @@ class Daily(BaseModel): class TradeSchema(BaseModel): - trade_id: str + trade_id: int pair: str is_open: bool exchange: str @@ -121,7 +121,7 @@ class TradeSchema(BaseModel): amount_requested: float stake_amount: float strategy: str - timeframe: str + timeframe: int fee_open: Optional[float] fee_open_cost: Optional[float] fee_open_currency: Optional[str] @@ -161,6 +161,20 @@ class TradeSchema(BaseModel): open_order_id: Optional[str] +class OpenTradeSchema(TradeSchema): + stoploss_current_dist: Optional[float] + stoploss_current_dist_pct: Optional[float] + stoploss_current_dist_ratio: Optional[float] + stoploss_entry_dist: Optional[float] + stoploss_entry_dist_ratio: Optional[float] + base_currency: str + current_profit: float + current_profit_abs: float + current_profit_pct: float + current_rate: float + open_order: Optional[str] + + class TradeResponse(BaseModel): trades: List[TradeSchema] trades_count: int diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 55bb9320c..1a067faf4 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -13,9 +13,10 @@ from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_models import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, Daily, DeleteTrade, ForceBuyPayload, ForceSellPayload, Locks, Logs, - PairHistory, PerformanceEntry, Ping, PlotConfig, - Profit, ResultMsg, Stats, StatusMsg, - StrategyListResponse, StrategyResponse, TradeResponse, TradeSchema, Version, + OpenTradeSchema, PairHistory, PerformanceEntry, + Ping, PlotConfig, Profit, ResultMsg, Stats, + StatusMsg, StrategyListResponse, StrategyResponse, + TradeResponse, TradeSchema, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc from freqtrade.rpc.rpc import RPCException @@ -73,8 +74,7 @@ def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_co config.get('fiat_display_currency', '')) -# TODO: Missing response model -@router.get('/status', response_model=List[TradeSchema], tags=['info']) +@router.get('/status', response_model=List[OpenTradeSchema], tags=['info']) def status(rpc: RPC = Depends(get_rpc)): try: return rpc._rpc_trade_status() diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index e84722041..fa9910b8d 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -32,6 +32,7 @@ def mock_trade_1(fee): exchange='bittrex', open_order_id='dry_run_buy_12345', strategy='DefaultStrategy', + timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') trade.orders.append(o) @@ -84,6 +85,7 @@ def mock_trade_2(fee): is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', + timeframe=5, sell_reason='sell_signal', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc), @@ -132,6 +134,7 @@ def mock_trade_3(fee): pair='XRP/BTC', stake_amount=0.001, amount=123.0, + amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.05, @@ -139,6 +142,8 @@ def mock_trade_3(fee): close_profit=0.01, exchange='bittrex', is_open=False, + strategy='DefaultStrategy', + timeframe=5, sell_reason='roi', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc), @@ -179,6 +184,7 @@ def mock_trade_4(fee): exchange='bittrex', open_order_id='prod_buy_12345', strategy='DefaultStrategy', + timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') trade.orders.append(o) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 1592fac10..cdd5c08d2 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -128,7 +128,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): if col not in ['index', 'open_at_end']: assert col in trades.columns trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='DefaultStrategy') - assert len(trades) == 3 + assert len(trades) == 4 trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy') assert len(trades) == 0 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5e972c694..f6e0ccd76 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -853,8 +853,8 @@ def test_api_forcebuy(botclient, mocker, fee): fee_close=fee.return_value, fee_open=fee.return_value, close_rate=0.265441, - id='22', - timeframe="5m", + id=22, + timeframe=5, strategy="DefaultStrategy" )) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) @@ -865,7 +865,7 @@ def test_api_forcebuy(botclient, mocker, fee): assert rc.json() == { 'amount': 1, 'amount_requested': 1, - 'trade_id': '22', + 'trade_id': 22, 'close_date': None, 'close_date_hum': None, 'close_timestamp': None, @@ -907,7 +907,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', - 'timeframe': '5m', + 'timeframe': 5, 'exchange': 'bittrex', } From 336dd1a29c1ac9467f4bfd5368aa83bf2f3288bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Jan 2021 09:07:31 +0100 Subject: [PATCH 114/563] Rename api_models to api_schemas --- freqtrade/rpc/api_server/api_auth.py | 2 +- .../api_server/{api_models.py => api_schemas.py} | 0 freqtrade/rpc/api_server/api_v1.py | 16 ++++++++-------- 3 files changed, 9 insertions(+), 9 deletions(-) rename freqtrade/rpc/api_server/{api_models.py => api_schemas.py} (100%) diff --git a/freqtrade/rpc/api_server/api_auth.py b/freqtrade/rpc/api_server/api_auth.py index 8d1316906..110bb2a25 100644 --- a/freqtrade/rpc/api_server/api_auth.py +++ b/freqtrade/rpc/api_server/api_auth.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from fastapi.security.http import HTTPBasic, HTTPBasicCredentials -from freqtrade.rpc.api_server.api_models import AccessAndRefreshToken, AccessToken +from freqtrade.rpc.api_server.api_schemas import AccessAndRefreshToken, AccessToken from freqtrade.rpc.api_server.deps import get_api_config diff --git a/freqtrade/rpc/api_server/api_models.py b/freqtrade/rpc/api_server/api_schemas.py similarity index 100% rename from freqtrade/rpc/api_server/api_models.py rename to freqtrade/rpc/api_server/api_schemas.py diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 1a067faf4..614563d01 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -10,14 +10,14 @@ from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.data.history import get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC -from freqtrade.rpc.api_server.api_models import (AvailablePairs, Balances, BlacklistPayload, - BlacklistResponse, Count, Daily, DeleteTrade, - ForceBuyPayload, ForceSellPayload, Locks, Logs, - OpenTradeSchema, PairHistory, PerformanceEntry, - Ping, PlotConfig, Profit, ResultMsg, Stats, - StatusMsg, StrategyListResponse, StrategyResponse, - TradeResponse, TradeSchema, Version, - WhitelistResponse) +from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, + BlacklistResponse, Count, Daily, DeleteTrade, + ForceBuyPayload, ForceSellPayload, Locks, Logs, + OpenTradeSchema, PairHistory, PerformanceEntry, + Ping, PlotConfig, Profit, ResultMsg, Stats, + StatusMsg, StrategyListResponse, StrategyResponse, + TradeResponse, TradeSchema, Version, + WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc from freqtrade.rpc.rpc import RPCException From 3dc37dd79d1f8a9af5ffa1a9f4b5b9d46c5093f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Jan 2021 12:54:40 +0100 Subject: [PATCH 115/563] Add types for deps --- freqtrade/rpc/api_server/deps.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index 691759012..ce6fbf4aa 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -1,13 +1,17 @@ +from typing import Any, Dict + +from freqtrade.rpc.rpc import RPC + from .webserver import ApiServer -def get_rpc(): +def get_rpc() -> RPC: return ApiServer._rpc -def get_config(): +def get_config() -> Dict[str, Any]: return ApiServer._config -def get_api_config(): +def get_api_config() -> Dict[str, Any]: return ApiServer._config['api_server'] From e6176d43f3b631b6969978274baef77178f2b905 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Jan 2021 13:12:49 +0100 Subject: [PATCH 116/563] Optional RPC dependency --- freqtrade/rpc/api_server/api_v1.py | 9 ++++++--- freqtrade/rpc/api_server/deps.py | 18 ++++++++++++++---- freqtrade/rpc/api_server/webserver.py | 1 + 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 614563d01..ddca88b06 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -18,7 +18,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac StatusMsg, StrategyListResponse, StrategyResponse, TradeResponse, TradeSchema, Version, WhitelistResponse) -from freqtrade.rpc.api_server.deps import get_config, get_rpc +from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -100,8 +100,11 @@ def edge(rpc: RPC = Depends(get_rpc)): # TODO: Missing response model @router.get('/show_config', tags=['info']) -def show_config(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): - return RPC._rpc_show_config(config, rpc._freqtrade.state) +def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(get_config)): + state = '' + if rpc: + state = rpc._freqtrade.state + return RPC._rpc_show_config(config, state) @router.post('/forcebuy', response_model=Union[TradeSchema, StatusMsg], tags=['trading']) diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index ce6fbf4aa..d2459010f 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -1,12 +1,22 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional -from freqtrade.rpc.rpc import RPC +from freqtrade.rpc.rpc import RPC, RPCException from .webserver import ApiServer -def get_rpc() -> RPC: - return ApiServer._rpc +def get_rpc_optional() -> Optional[RPC]: + if ApiServer._has_rpc: + return ApiServer._rpc + return None + + +def get_rpc() -> Optional[RPC]: + _rpc = get_rpc_optional() + if _rpc: + return _rpc + else: + raise RPCException('Bot is not in the correct state') def get_config() -> Dict[str, Any]: diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index ac7a35a9e..a475ad494 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -17,6 +17,7 @@ logger = logging.getLogger(__name__) class ApiServer(RPCHandler): _rpc: RPC + _has_rpc: bool = False _config: Dict[str, Any] = {} def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: From ca0bb7bbb822498cf3c219faad37d8b8f5b1c2e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Jan 2021 15:11:40 +0100 Subject: [PATCH 117/563] Don't require RPC for strategy --- freqtrade/rpc/api_server/api_schemas.py | 10 ++++++++-- freqtrade/rpc/api_server/api_v1.py | 20 ++++++++++---------- freqtrade/rpc/api_server/webserver.py | 1 + 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 8c2a25ceb..60b2970eb 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,5 +1,5 @@ from datetime import date, datetime -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, TypeVar, Union from pydantic import BaseModel @@ -180,6 +180,9 @@ class TradeResponse(BaseModel): trades_count: int +ForceBuyResponse = TypeVar('ForceBuyResponse', TradeSchema, StatusMsg) + + class LockModel(BaseModel): active: bool lock_end_time: str @@ -234,11 +237,14 @@ class DeleteTrade(BaseModel): trade_id: int -class PlotConfig(BaseModel): +class PlotConfig_(BaseModel): main_plot: Dict[str, Any] subplots: Optional[Dict[str, Any]] +PlotConfig = TypeVar('PlotConfig', PlotConfig_, Dict) + + class StrategyListResponse(BaseModel): strategies: List[str] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index ddca88b06..ab68c3b68 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -1,6 +1,6 @@ from copy import deepcopy from pathlib import Path -from typing import Dict, List, Optional, Union +from typing import List, Optional from fastapi import APIRouter, Depends from fastapi.exceptions import HTTPException @@ -12,12 +12,12 @@ from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, Daily, DeleteTrade, - ForceBuyPayload, ForceSellPayload, Locks, Logs, - OpenTradeSchema, PairHistory, PerformanceEntry, - Ping, PlotConfig, Profit, ResultMsg, Stats, - StatusMsg, StrategyListResponse, StrategyResponse, - TradeResponse, TradeSchema, Version, - WhitelistResponse) + ForceBuyPayload, ForceBuyResponse, + ForceSellPayload, Locks, Logs, OpenTradeSchema, + PairHistory, PerformanceEntry, Ping, PlotConfig, + Profit, ResultMsg, Stats, StatusMsg, + StrategyListResponse, StrategyResponse, + TradeResponse, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -107,7 +107,7 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g return RPC._rpc_show_config(config, state) -@router.post('/forcebuy', response_model=Union[TradeSchema, StatusMsg], tags=['trading']) +@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading']) def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): trade = rpc._rpc_forcebuy(payload.pair, payload.price) @@ -182,7 +182,7 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) -@router.get('/plot_config', response_model=Union[PlotConfig, Dict], tags=['candle data']) +@router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) def plot_config(rpc: RPC = Depends(get_rpc)): return rpc._rpc_plot_config() @@ -199,7 +199,7 @@ def list_strategies(config=Depends(get_config)): @router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy']) -def get_strategy(strategy: str, rpc: RPC = Depends(get_rpc), config=Depends(get_config)): +def get_strategy(strategy: str, config=Depends(get_config)): config = deepcopy(config) from freqtrade.resolvers.strategy_resolver import StrategyResolver diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index a475ad494..f54a96f1b 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -25,6 +25,7 @@ class ApiServer(RPCHandler): self._server = None ApiServer._rpc = rpc + ApiServer._has_rpc = True ApiServer._config = config api_config = self._config['api_server'] From cff50f9f66e68d34a05ec7a2ac732e56eb8663c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Jan 2021 15:48:33 +0100 Subject: [PATCH 118/563] Add response-model for show_config --- freqtrade/rpc/api_server/api_schemas.py | 23 +++++++++++++++++++++++ freqtrade/rpc/api_server/api_v1.py | 5 ++--- freqtrade/rpc/rpc.py | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 60b2970eb..16a1c263d 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -112,6 +112,29 @@ class Daily(BaseModel): stake_currency: str +class ShowConfig(BaseModel): + dry_run: str + stake_currency: str + stake_amount: float + max_open_trades: int + minimal_roi: Dict[str, Any] + stoploss: float + trailing_stop: bool + trailing_stop_positive: Optional[float] + trailing_stop_positive_offset: Optional[float] + trailing_only_offset_is_reached: Optional[bool] + timeframe: str + timeframe_ms: int + timeframe_min: int + exchange: str + strategy: str + forcebuy_enabled: bool + ask_strategy: Dict[str, Any] + bid_strategy: Dict[str, Any] + state: str + runmode: str + + class TradeSchema(BaseModel): trade_id: int pair: str diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index ab68c3b68..a2082103b 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -15,7 +15,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac ForceBuyPayload, ForceBuyResponse, ForceSellPayload, Locks, Logs, OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, - Profit, ResultMsg, Stats, StatusMsg, + Profit, ResultMsg, ShowConfig, Stats, StatusMsg, StrategyListResponse, StrategyResponse, TradeResponse, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional @@ -98,8 +98,7 @@ def edge(rpc: RPC = Depends(get_rpc)): return rpc._rpc_edge() -# TODO: Missing response model -@router.get('/show_config', tags=['info']) +@router.get('/show_config', response_model=ShowConfig, tags=['info']) def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(get_config)): state = '' if rpc: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 70a99a186..4ad2b3485 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -111,7 +111,7 @@ class RPC: self._fiat_converter = CryptoToFiatConverter() @staticmethod - def _rpc_show_config(config, botstate: State) -> Dict[str, Any]: + def _rpc_show_config(config, botstate: Union[State, str]) -> Dict[str, Any]: """ Return a dict of config options. Explicitly does NOT return the full config to avoid leakage of sensitive From 26c34634039fcf61f51bf7e7b0e95da7334ed719 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Jan 2021 06:46:06 +0100 Subject: [PATCH 119/563] Stake-amount supports unlimited, too --- freqtrade/rpc/api_server/api_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 16a1c263d..45f160008 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -115,7 +115,7 @@ class Daily(BaseModel): class ShowConfig(BaseModel): dry_run: str stake_currency: str - stake_amount: float + stake_amount: Union[float, str] max_open_trades: int minimal_roi: Dict[str, Any] stoploss: float From 634d6f38989288c383f98a706f3023e9a06005b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Jan 2021 07:15:45 +0100 Subject: [PATCH 120/563] Change logging to stderr --- freqtrade/rpc/api_server/webserver.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index f54a96f1b..d4c34f2f9 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -99,9 +99,14 @@ class ApiServer(RPCHandler): logger.info('Starting Local Rest Server.') verbosity = self._config['api_server'].get('verbosity', 'info') + log_config = uvicorn.config.LOGGING_CONFIG + # Change logging of access logs to stderr + log_config["handlers"]["access"]["stream"] = log_config["handlers"]["default"]["stream"] uvconfig = uvicorn.Config(self.app, port=rest_port, host=rest_ip, + use_colors=False, + log_config=log_config, access_log=True if verbosity != 'error' else False, ) try: From 5ca2cd3a1e0fe4ac51a82fc0e7439962ba697349 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Jan 2021 07:18:41 +0100 Subject: [PATCH 121/563] Change defaults to log only errors --- config.json.example | 6 +++--- config_binance.json.example | 6 +++--- config_full.json.example | 2 +- config_kraken.json.example | 6 +++--- docs/rest-api.md | 2 +- freqtrade/rpc/api_server/webserver.py | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/config.json.example b/config.json.example index af45dac74..fc59a4d5b 100644 --- a/config.json.example +++ b/config.json.example @@ -79,11 +79,11 @@ "enabled": false, "listen_ip_address": "127.0.0.1", "listen_port": 8080, - "verbosity": "info", + "verbosity": "error", "jwt_secret_key": "somethingrandom", "CORS_origins": [], - "username": "", - "password": "" + "username": "freqtrader", + "password": "SuperSecurePassword" }, "initial_state": "running", "forcebuy_enable": false, diff --git a/config_binance.json.example b/config_binance.json.example index f3f8eb659..954634def 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -84,11 +84,11 @@ "enabled": false, "listen_ip_address": "127.0.0.1", "listen_port": 8080, - "verbosity": "info", + "verbosity": "error", "jwt_secret_key": "somethingrandom", "CORS_origins": [], - "username": "", - "password": "" + "username": "freqtrader", + "password": "SuperSecurePassword" }, "initial_state": "running", "forcebuy_enable": false, diff --git a/config_full.json.example b/config_full.json.example index ef9f45363..7cdd6af67 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -164,7 +164,7 @@ "enabled": false, "listen_ip_address": "127.0.0.1", "listen_port": 8080, - "verbosity": "info", + "verbosity": "error", "enable_openapi": false, "jwt_secret_key": "somethingrandom", "CORS_origins": [], diff --git a/config_kraken.json.example b/config_kraken.json.example index 5f3b57854..4b33eb592 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -89,11 +89,11 @@ "enabled": false, "listen_ip_address": "127.0.0.1", "listen_port": 8080, - "verbosity": "info", + "verbosity": "error", "jwt_secret_key": "somethingrandom", "CORS_origins": [], - "username": "", - "password": "" + "username": "freqtrader", + "password": "SuperSecurePassword" }, "initial_state": "running", "forcebuy_enable": false, diff --git a/docs/rest-api.md b/docs/rest-api.md index 279373c50..a013bf358 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -11,7 +11,7 @@ Sample configuration: "enabled": true, "listen_ip_address": "127.0.0.1", "listen_port": 8080, - "verbosity": "info", + "verbosity": "error", "enable_openapi": false, "jwt_secret_key": "somethingrandom", "CORS_origins": [], diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index d4c34f2f9..97dfa444d 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -31,7 +31,7 @@ class ApiServer(RPCHandler): self.app = FastAPI(title="Freqtrade API", openapi_url='openapi.json' if api_config.get( - 'enable_openapi') else None, + 'enable_openapi', False) else None, redoc_url=None, ) self.configure_app(self.app, self._config) @@ -98,7 +98,7 @@ class ApiServer(RPCHandler): "Others may be able to log into your bot.") logger.info('Starting Local Rest Server.') - verbosity = self._config['api_server'].get('verbosity', 'info') + verbosity = self._config['api_server'].get('verbosity', 'error') log_config = uvicorn.config.LOGGING_CONFIG # Change logging of access logs to stderr log_config["handlers"]["access"]["stream"] = log_config["handlers"]["default"]["stream"] From 66391b80ae6c8b87c96e819e894e32d4c83b167e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 05:38:57 +0000 Subject: [PATCH 122/563] Bump isort from 5.6.4 to 5.7.0 Bumps [isort](https://github.com/pycqa/isort) from 5.6.4 to 5.7.0. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.6.4...5.7.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index a2da87430..e918a9b90 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.4.0 pytest-random-order==1.0.4 -isort==5.6.4 +isort==5.7.0 # Convert jupyter notebooks to markdown documents nbconvert==6.0.7 From 7d06e61461b65be63d99e17a62828fd1e1c63947 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 05:38:57 +0000 Subject: [PATCH 123/563] Bump scipy from 1.5.4 to 1.6.0 Bumps [scipy](https://github.com/scipy/scipy) from 1.5.4 to 1.6.0. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.5.4...v1.6.0) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index a2446ddb8..fbb963cf9 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.5.4 +scipy==1.6.0 scikit-learn==0.24.0 scikit-optimize==0.8.1 filelock==3.0.12 From 9e435fba0bd22e2515e9f01b6aa6c2dc9e9fc721 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 05:39:14 +0000 Subject: [PATCH 124/563] Bump ccxt from 1.39.79 to 1.40.14 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.39.79 to 1.40.14. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.39.79...1.40.14) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bab13ed03..383c9686e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.2.0 -ccxt==1.39.79 +ccxt==1.40.14 aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 From d1804dee6b2675e1353919eef71f965a91093e05 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 4 Jan 2021 09:40:17 +0100 Subject: [PATCH 125/563] Add note about python-dev dependency --- docs/installation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index be98c45a8..73e791c56 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -34,7 +34,8 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). !!! Note - Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. + Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. + Also, python headers (`python-dev` / `python-devel`) must be available for the installation to complete successfully. This can be achieved with the following commands: From 07bc0c3fce4b3ed6557e93b251025ce9e2119387 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 4 Jan 2021 13:47:16 +0100 Subject: [PATCH 126/563] Improve merge_informative_pairs to properly merge correct timeframes explanation in #4073, closes #4073 --- freqtrade/strategy/strategy_helper.py | 13 +++++++++++-- tests/strategy/test_strategy_helpers.py | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index ea0e234ec..d7b1327d9 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -24,15 +24,24 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param timeframe: Timeframe of the original pair sample. :param timeframe_inf: Timeframe of the informative pair sample. :param ffill: Forwardfill missing values - optional but usually required + :return: Merged dataframe + :raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe """ minutes_inf = timeframe_to_minutes(timeframe_inf) minutes = timeframe_to_minutes(timeframe) - if minutes >= minutes_inf: + if minutes == minutes_inf: # No need to forwardshift if the timeframes are identical informative['date_merge'] = informative["date"] + elif minutes < minutes_inf: + # Subtract "small" timeframe so merging is not delayed by 1 small candle + # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 + informative['date_merge'] = ( + informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm') + ) else: - informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes_inf, 'm') + raise ValueError("Tried to merge a faster timeframe to a slower timeframe." + "This would create new rows, and can throw off your regular indicators.") # Rename columns to be unique informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 1d3e80d24..252288e2e 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -1,5 +1,6 @@ import numpy as np import pandas as pd +import pytest from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes @@ -47,17 +48,17 @@ def test_merge_informative_pair(): assert 'volume_1h' in result.columns assert result['volume'].equals(data['volume']) - # First 4 rows are empty + # First 3 rows are empty assert result.iloc[0]['date_1h'] is pd.NaT assert result.iloc[1]['date_1h'] is pd.NaT assert result.iloc[2]['date_1h'] is pd.NaT - assert result.iloc[3]['date_1h'] is pd.NaT # Next 4 rows contain the starting date (0:00) + assert result.iloc[3]['date_1h'] == result.iloc[0]['date'] assert result.iloc[4]['date_1h'] == result.iloc[0]['date'] assert result.iloc[5]['date_1h'] == result.iloc[0]['date'] assert result.iloc[6]['date_1h'] == result.iloc[0]['date'] - assert result.iloc[7]['date_1h'] == result.iloc[0]['date'] # Next 4 rows contain the next Hourly date original date row 4 + assert result.iloc[7]['date_1h'] == result.iloc[4]['date'] assert result.iloc[8]['date_1h'] == result.iloc[4]['date'] @@ -86,3 +87,11 @@ def test_merge_informative_pair_same(): # Dates match 1:1 assert result['date_15m'].equals(result['date']) + + +def test_merge_informative_pair_lower(): + data = generate_test_data('1h', 40) + informative = generate_test_data('15m', 40) + + with pytest.raises(ValueError, match=r"Tried to merge a faster timeframe .*"): + merge_informative_pair(data, informative, '1h', '15m', ffill=True) From 0704cfb05b611a2f88bf5b44d02df273d1d8e628 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Mon, 4 Jan 2021 14:14:52 +0100 Subject: [PATCH 127/563] Added an example with a positive offset for a custom stoploss Signed-off-by: hoeckxer --- docs/strategy-advanced.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index b1fcb50fc..9d5930518 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -119,6 +119,41 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` +#### Trailing stoploss with positive offset + +Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. + +Please note that the stoploss can only increase, values lower than the current stoploss are ignored. + +``` python +from datetime import datetime, timedelta +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: + + if current_profit < 0.04: + return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss + + # After reaching the desired offset, allow the stoploss to trail by half the profit + stoploss = current_profit / 2 + + if abs(stoploss) < 0.025: + # Maintain a minimum of 2.5% trailing stoploss + stoploss = 0.025 + if abs(stoploss) > 0.05: + # Maximize the stoploss at 5% + stoploss = 0.05 + + return stoploss +``` + #### Absolute stoploss The below example sets absolute profit levels based on the current profit. From 1cf6e2c957de86054d5611ddf78230754e3fb112 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Mon, 4 Jan 2021 14:37:22 +0100 Subject: [PATCH 128/563] Changed documentation based on review comments Signed-off-by: hoeckxer --- docs/strategy-advanced.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 9d5930518..2431274d7 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -142,16 +142,10 @@ class AwesomeStrategy(IStrategy): return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss # After reaching the desired offset, allow the stoploss to trail by half the profit - stoploss = current_profit / 2 + desired_stoploss = current_profit / 2 - if abs(stoploss) < 0.025: - # Maintain a minimum of 2.5% trailing stoploss - stoploss = 0.025 - if abs(stoploss) > 0.05: - # Maximize the stoploss at 5% - stoploss = 0.05 - - return stoploss + # Use a minimum of 2.5% and a maximum of 5% + return max(min(desired_stoploss, 0.05), 0.025) ``` #### Absolute stoploss From 614a99659762c2b5594fcca1ddfc1c1458a732ae Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Mon, 4 Jan 2021 20:49:24 +0100 Subject: [PATCH 129/563] First commit about ignoring expired candle Signed-off-by: hoeckxer --- docs/configuration.md | 17 +++++++++++++++++ freqtrade/strategy/interface.py | 21 ++++++++++++++++++++- tests/strategy/test_interface.py | 17 +++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index b70a85c04..db078cba8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -121,6 +121,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `user_data_dir` | Directory containing user data.
*Defaults to `./user_data/`*.
**Datatype:** String | `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data.
*Defaults to `json`*.
**Datatype:** String | `dataformat_trades` | Data format to use to store historical trades data.
*Defaults to `jsongz`*.
**Datatype:** String +| `ignore_buying_expired_candle` | Enables usage of skipping buys on candles that are older than a specified period.
*Defaults to `False`*
**Datatype:** Boolean +| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used when setting `ignore_buying_expired_candle`.
**Datatype:** Integer ### Parameters in the strategy @@ -144,6 +146,8 @@ Values set in the configuration file always overwrite values set in the strategy * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) +* `ignore_buying_expired_candle` +* `ignore_buying_expired_candle_after` ### Configuring amount per trade @@ -671,6 +675,19 @@ export HTTPS_PROXY="http://addr:port" freqtrade ``` +## Ignoring expired candles + +When working with larger timeframes (for example 1h or more) and using a low `max_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it. + +In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ignore_buying_expired_candle` to `True`. After this, you can set `ignore_buying_expired_candle_after` to the number of seconds after which the candle becomes expired. + +For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy: + +``` json +ignore_buying_expired_candle = True +ignore_buying_expired_candle_after = 300 # 5 minutes +``` + ## Embedding Strategies Freqtrade provides you with with an easy way to embed the strategy into your configuration file. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1e4fc8b12..62ef4e91b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -5,7 +5,7 @@ This module defines the interface to apply for strategies import logging import warnings from abc import ABC, abstractmethod -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta from enum import Enum from typing import Dict, List, NamedTuple, Optional, Tuple @@ -113,6 +113,11 @@ class IStrategy(ABC): # run "populate_indicators" only for new candle process_only_new_candles: bool = False + # Don't analyze too old candles + ignore_buying_expired_candle: bool = False + # Number of seconds after which the candle will no longer result in a buy + ignore_buying_expired_candle_after: int = 0 + # Disable checking the dataframe (converts the error into a warning message) disable_dataframe_checks: bool = False @@ -476,8 +481,21 @@ class IStrategy(ABC): (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) + if self.ignore_expired_candle(dataframe=dataframe, buy=buy): + return False, sell return buy, sell + def ignore_expired_candle(self, dataframe: DataFrame, buy: bool): + if self.ignore_buying_expired_candle and buy: + current_time = datetime.now(timezone.utc) - timedelta(seconds=self.ignore_buying_expired_candle_after) + candle_time = dataframe['date'].tail(1).iat[0] + time_delta = current_time - candle_time + if time_delta.total_seconds() > self.ignore_buying_expired_candle_after: + logger.debug('ignoring buy signals because candle exceeded ignore_buying_expired_candle_after of %s seconds', self.ignore_buying_expired_candle_after) + return True + else: + return False + def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: @@ -672,6 +690,7 @@ class IStrategy(ABC): :return: DataFrame with buy column """ logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.") + if self._buy_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 7eed43302..330689039 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -106,6 +106,23 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) +def test_ignore_expired_candle(default_conf, ohlcv_history): + default_conf.update({'strategy': 'DefaultStrategy'}) + strategy = StrategyResolver.load_strategy(default_conf) + strategy.ignore_buying_expired_candle = True + strategy.ignore_buying_expired_candle_after = 60 + + ohlcv_history.loc[-1, 'date'] = arrow.utcnow().shift(minutes=-3) + # Take a copy to correctly modify the call + mocked_history = ohlcv_history.copy() + mocked_history['sell'] = 0 + mocked_history['buy'] = 0 + mocked_history.loc[1, 'buy'] = 1 + mocked_history.loc[1, 'sell'] = 1 + + assert strategy.ignore_expired_candle(mocked_history, True) == True + + def test_assert_df_raise(mocker, caplog, ohlcv_history): ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) # Take a copy to correctly modify the call From 844df96ec77b7e9f28969627e26bc53253189f76 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Tue, 5 Jan 2021 07:06:53 +0100 Subject: [PATCH 130/563] Making changes so the build checks are satisified (imports & flake8) Signed-off-by: hoeckxer --- freqtrade/strategy/interface.py | 10 +++++++--- tests/strategy/test_interface.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 62ef4e91b..5f7ef8590 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -5,7 +5,7 @@ This module defines the interface to apply for strategies import logging import warnings from abc import ABC, abstractmethod -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from enum import Enum from typing import Dict, List, NamedTuple, Optional, Tuple @@ -487,11 +487,15 @@ class IStrategy(ABC): def ignore_expired_candle(self, dataframe: DataFrame, buy: bool): if self.ignore_buying_expired_candle and buy: - current_time = datetime.now(timezone.utc) - timedelta(seconds=self.ignore_buying_expired_candle_after) + current_time = datetime.now(timezone.utc) - timedelta( + seconds=self.ignore_buying_expired_candle_after) candle_time = dataframe['date'].tail(1).iat[0] time_delta = current_time - candle_time if time_delta.total_seconds() > self.ignore_buying_expired_candle_after: - logger.debug('ignoring buy signals because candle exceeded ignore_buying_expired_candle_after of %s seconds', self.ignore_buying_expired_candle_after) + logger.debug( + '''ignoring buy signals because candle exceeded + ignore_buying_expired_candle_after of %s seconds''', + self.ignore_buying_expired_candle_after) return True else: return False diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 330689039..f389be45b 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -120,7 +120,7 @@ def test_ignore_expired_candle(default_conf, ohlcv_history): mocked_history.loc[1, 'buy'] = 1 mocked_history.loc[1, 'sell'] = 1 - assert strategy.ignore_expired_candle(mocked_history, True) == True + assert strategy.ignore_expired_candle(mocked_history, True) is True def test_assert_df_raise(mocker, caplog, ohlcv_history): From 9a93a0876ab7080cfa4eb424fff8a1f7eeb92d29 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 5 Jan 2021 07:32:07 +0100 Subject: [PATCH 131/563] Update interface.py Adjusted comment --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 5f7ef8590..4d6e327f3 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -113,7 +113,7 @@ class IStrategy(ABC): # run "populate_indicators" only for new candle process_only_new_candles: bool = False - # Don't analyze too old candles + # Don't buy on expired candles ignore_buying_expired_candle: bool = False # Number of seconds after which the candle will no longer result in a buy ignore_buying_expired_candle_after: int = 0 From 67306d943a0e7b0a86d799cc6022ab117358369b Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 5 Jan 2021 07:33:34 +0100 Subject: [PATCH 132/563] Update interface.py Simplified return value, thereby including the situation where the time simply hasn't expired yet --- freqtrade/strategy/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4d6e327f3..41fa26ac4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -497,8 +497,8 @@ class IStrategy(ABC): ignore_buying_expired_candle_after of %s seconds''', self.ignore_buying_expired_candle_after) return True - else: - return False + + return False def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool, low: float = None, high: float = None, From c9ed2137bb3b324a40d70110e0d5b959744fac10 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Tue, 5 Jan 2021 09:07:46 +0100 Subject: [PATCH 133/563] Simplified return statements Signed-off-by: hoeckxer --- freqtrade/strategy/interface.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 41fa26ac4..a1da3681d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -491,14 +491,9 @@ class IStrategy(ABC): seconds=self.ignore_buying_expired_candle_after) candle_time = dataframe['date'].tail(1).iat[0] time_delta = current_time - candle_time - if time_delta.total_seconds() > self.ignore_buying_expired_candle_after: - logger.debug( - '''ignoring buy signals because candle exceeded - ignore_buying_expired_candle_after of %s seconds''', - self.ignore_buying_expired_candle_after) - return True - - return False + return time_delta.total_seconds() > self.ignore_buying_expired_candle_after + else: + return False def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool, low: float = None, high: float = None, From eaaaddac86cf1f480fc34c6a0d8d33f3bffbceea Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 5 Jan 2021 11:10:00 +0100 Subject: [PATCH 134/563] Update docs/configuration.md Co-authored-by: Matthias --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index db078cba8..c1c2e65ec 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -677,7 +677,7 @@ freqtrade ## Ignoring expired candles -When working with larger timeframes (for example 1h or more) and using a low `max_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it. +When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it. In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ignore_buying_expired_candle` to `True`. After this, you can set `ignore_buying_expired_candle_after` to the number of seconds after which the candle becomes expired. From e3f3f3629828242167aac1d7cd65939fa60fbfd6 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Tue, 5 Jan 2021 14:49:35 +0100 Subject: [PATCH 135/563] Changes based on review comments --- freqtrade/resolvers/strategy_resolver.py | 2 ++ freqtrade/strategy/interface.py | 10 +++++----- tests/strategy/test_interface.py | 9 +++------ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 73af00fee..5872d95a6 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -79,6 +79,8 @@ class StrategyResolver(IResolver): ("sell_profit_only", False, 'ask_strategy'), ("ignore_roi_if_buy_signal", False, 'ask_strategy'), ("disable_dataframe_checks", False, None), + ("ignore_buying_expired_candle", None, 'ask_strategy'), + ("ignore_buying_expired_candle_after", 0, 'ask_strategy') ] for attribute, default, subkey in attributes: if subkey: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a1da3681d..8976b2fd5 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -15,7 +15,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException, StrategyError -from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper @@ -481,16 +481,16 @@ class IStrategy(ABC): (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) - if self.ignore_expired_candle(dataframe=dataframe, buy=buy): + timeframe_seconds = timeframe_to_seconds(timeframe) + if self.ignore_expired_candle(latest_date=latest_date, timeframe_seconds=timeframe_seconds, buy=buy): return False, sell return buy, sell - def ignore_expired_candle(self, dataframe: DataFrame, buy: bool): + def ignore_expired_candle(self, latest_date: datetime, timeframe_seconds: int, buy: bool): if self.ignore_buying_expired_candle and buy: current_time = datetime.now(timezone.utc) - timedelta( seconds=self.ignore_buying_expired_candle_after) - candle_time = dataframe['date'].tail(1).iat[0] - time_delta = current_time - candle_time + time_delta = current_time - latest_date + timedelta(seconds=timeframe_seconds) return time_delta.total_seconds() > self.ignore_buying_expired_candle_after else: return False diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f389be45b..af086b0da 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -112,15 +112,12 @@ def test_ignore_expired_candle(default_conf, ohlcv_history): strategy.ignore_buying_expired_candle = True strategy.ignore_buying_expired_candle_after = 60 - ohlcv_history.loc[-1, 'date'] = arrow.utcnow().shift(minutes=-3) + ohlcv_history.loc[-1, 'date'] = arrow.utcnow() # Take a copy to correctly modify the call mocked_history = ohlcv_history.copy() - mocked_history['sell'] = 0 - mocked_history['buy'] = 0 - mocked_history.loc[1, 'buy'] = 1 - mocked_history.loc[1, 'sell'] = 1 + latest_date = mocked_history['date'].max() - assert strategy.ignore_expired_candle(mocked_history, True) is True + assert strategy.ignore_expired_candle(latest_date=latest_date, timeframe_seconds=300, buy=True) is True def test_assert_df_raise(mocker, caplog, ohlcv_history): From 573de1cf08a323accee427bd168a8bbd0fec7430 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Tue, 5 Jan 2021 15:30:29 +0100 Subject: [PATCH 136/563] Fixed flake8 warnings --- freqtrade/strategy/interface.py | 5 +++-- tests/strategy/test_interface.py | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 8976b2fd5..2b4a8dd03 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -21,7 +21,6 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets - logger = logging.getLogger(__name__) @@ -482,7 +481,9 @@ class IStrategy(ABC): logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) timeframe_seconds = timeframe_to_seconds(timeframe) - if self.ignore_expired_candle(latest_date=latest_date, timeframe_seconds=timeframe_seconds, buy=buy): + if self.ignore_expired_candle(latest_date=latest_date, + timeframe_seconds=timeframe_seconds, + buy=buy): return False, sell return buy, sell diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index af086b0da..d7d113a4e 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -117,7 +117,9 @@ def test_ignore_expired_candle(default_conf, ohlcv_history): mocked_history = ohlcv_history.copy() latest_date = mocked_history['date'].max() - assert strategy.ignore_expired_candle(latest_date=latest_date, timeframe_seconds=300, buy=True) is True + assert strategy.ignore_expired_candle(latest_date=latest_date, + timeframe_seconds=300, + buy=True) is True def test_assert_df_raise(mocker, caplog, ohlcv_history): From 65d91a3a58c7807f2e9b18a85d780ba7ea4dc6a4 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Tue, 5 Jan 2021 15:36:34 +0100 Subject: [PATCH 137/563] isort fix --- freqtrade/strategy/interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2b4a8dd03..a18c6c915 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -21,6 +21,7 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) From 5c34140a191a96a82e8ca617bf10d4b16f909b90 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Tue, 5 Jan 2021 20:59:31 +0100 Subject: [PATCH 138/563] Adjusted documentation to reflect sub-key configuration --- docs/configuration.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c1c2e65ec..6ae37e7b3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -74,6 +74,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
**Datatype:** Boolean | `ask_strategy.sell_profit_only` | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean +| `ask_strategy.ignore_buying_expired_candle` | Enables usage of skipping buys on candles that are older than a specified period.
*Defaults to `False`*
**Datatype:** Boolean +| `ask_strategy.ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used when setting `ignore_buying_expired_candle`.
**Datatype:** Integer | `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
**Datatype:** String @@ -121,8 +123,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `user_data_dir` | Directory containing user data.
*Defaults to `./user_data/`*.
**Datatype:** String | `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data.
*Defaults to `json`*.
**Datatype:** String | `dataformat_trades` | Data format to use to store historical trades data.
*Defaults to `jsongz`*.
**Datatype:** String -| `ignore_buying_expired_candle` | Enables usage of skipping buys on candles that are older than a specified period.
*Defaults to `False`*
**Datatype:** Boolean -| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used when setting `ignore_buying_expired_candle`.
**Datatype:** Integer ### Parameters in the strategy @@ -146,8 +146,8 @@ Values set in the configuration file always overwrite values set in the strategy * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) -* `ignore_buying_expired_candle` -* `ignore_buying_expired_candle_after` +* `ignore_buying_expired_candle` (ask_strategy) +* `ignore_buying_expired_candle_after` (ask_strategy) ### Configuring amount per trade @@ -679,13 +679,17 @@ freqtrade When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it. -In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ignore_buying_expired_candle` to `True`. After this, you can set `ignore_buying_expired_candle_after` to the number of seconds after which the candle becomes expired. +In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle` to `true`. After this, you can set `ask_strategy.ignore_buying_expired_candle_after` to the number of seconds after which the candle becomes expired. For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy: -``` json -ignore_buying_expired_candle = True -ignore_buying_expired_candle_after = 300 # 5 minutes +``` jsonc + "ask_strategy":{ + "ignore_buying_expired_candle" = true + "ignore_buying_expired_candle_after" = 300 # 5 minutes + "price_side": "bid", + // ... + }, ``` ## Embedding Strategies From 95732e8991094668ab2cc43224503177709905a9 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Tue, 5 Jan 2021 21:03:23 +0100 Subject: [PATCH 139/563] Clarification in documentation Signed-off-by: hoeckxer --- docs/includes/protections.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 87db17fd8..7d2107bee 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -62,9 +62,9 @@ The below example stops trading for all pairs for 4 candles after the last trade #### MaxDrawdown -`MaxDrawdown` uses all trades within `lookback_period` (in minutes) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` (in minutes) after the last trade - assuming that the bot needs some time to let markets recover. +`MaxDrawdown` uses all trades within `lookback_period` (in minutes or candles) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` (in minutes or candles) after the last trade - assuming that the bot needs some time to let markets recover. -The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades within the last 48 candles. +The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades within the last 48 candles. If desired, `lookback_period_minutes` and/or `stop_duration_minutes` can be used. ```json "protections": [ From f7b055a58c9573778af3f517cc57b36873cf2e1a Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Wed, 6 Jan 2021 09:26:03 +0100 Subject: [PATCH 140/563] Attempt to improve wording Signed-off-by: hoeckxer --- docs/includes/protections.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 7d2107bee..560100e4d 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -62,9 +62,9 @@ The below example stops trading for all pairs for 4 candles after the last trade #### MaxDrawdown -`MaxDrawdown` uses all trades within `lookback_period` (in minutes or candles) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` (in minutes or candles) after the last trade - assuming that the bot needs some time to let markets recover. +`MaxDrawdown` uses all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after the last trade - assuming that the bot needs some time to let markets recover. -The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades within the last 48 candles. If desired, `lookback_period_minutes` and/or `stop_duration_minutes` can be used. +The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. ```json "protections": [ From a90609315307583738fee87dd565cec799edfbfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Jan 2021 09:45:21 +0100 Subject: [PATCH 141/563] FIx doc wording for all guards --- docs/includes/protections.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 560100e4d..b98a0d662 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -39,7 +39,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex #### Stoploss Guard -`StoplossGuard` selects all trades within `lookback_period`, and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. +`StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`). This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles. @@ -81,8 +81,8 @@ The below sample stops trading for 12 candles if max-drawdown is > 20% consideri #### Low Profit Pairs -`LowProfitPairs` uses all trades for a pair within `lookback_period` (in minutes) to determine the overall profit ratio. -If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). +`LowProfitPairs` uses all trades for a pair within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the overall profit ratio. +If that ratio is below `required_profit`, that pair will be locked for `stop_duration` in minutes (or in candles when using `stop_duration_candles`). The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles. @@ -100,7 +100,7 @@ The below example will stop trading a pair for 60 minutes if the pair does not h #### Cooldown Period -`CooldownPeriod` locks a pair for `stop_duration` (in minutes) after selling, avoiding a re-entry for this pair for `stop_duration` minutes. +`CooldownPeriod` locks a pair for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after selling, avoiding a re-entry for this pair for `stop_duration` minutes. The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down". From 91f86678817975d8c35d32bc502103a45e601a0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Jan 2021 09:57:36 +0100 Subject: [PATCH 142/563] DOn't update open orders in dry-run mode --- freqtrade/freqtradebot.py | 5 +++++ tests/test_freqtradebot.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d60b111f2..6dc8eacf9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -246,6 +246,10 @@ class FreqtradeBot(LoggingMixin): Updates open orders based on order list kept in the database. Mainly updates the state of orders - but may also close trades """ + if self.config['dry_run']: + # Updating open orders in dry-run does not make sense and will fail. + return + orders = Order.get_open_orders() logger.info(f"Updating {len(orders)} open orders.") for order in orders: @@ -256,6 +260,7 @@ class FreqtradeBot(LoggingMixin): self.update_trade_state(order.trade, order.order_id, fo) except ExchangeError as e: + logger.warning(f"Error updating Order {order.order_id} due to {e}") def update_closed_trades_without_assigned_fees(self): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 12be5ae8b..5c5666788 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4313,6 +4313,11 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): create_mock_trades(fee) freqtrade.update_open_orders() + assert not log_has_re(r"Error updating Order .*", caplog) + + freqtrade.config['dry_run'] = False + freqtrade.update_open_orders() + assert log_has_re(r"Error updating Order .*", caplog) caplog.clear() From a9ca72c1b8e489117d9e737592da7240bc3ef1b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Jan 2021 11:04:14 +0100 Subject: [PATCH 143/563] Fix typo in documentation --- docs/docker_quickstart.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 48ee34954..e25e1b050 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -123,7 +123,7 @@ Advanced users may edit the docker-compose file further to include all possible All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. !!! Note "`docker-compose run --rm`" - Including `--rm` will clean up the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). + Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). #### Example: Download data with docker-compose @@ -172,19 +172,19 @@ docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p B The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser. -## Data analayis using docker compose +## Data analysis using docker compose Freqtrade provides a docker-compose file which starts up a jupyter lab server. You can run this server using the following command: ``` bash -docker-compose --rm -f docker/docker-compose-jupyter.yml up +docker-compose -f docker/docker-compose-jupyter.yml up ``` -This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. +This will create a docker-container running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. Please use the link that's printed in the console after startup for simplified login. -Since part of this image is built on your machine, it is recommended to rebuild the image from time to time to keep freqtrade (and dependencies) uptodate. +Since part of this image is built on your machine, it is recommended to rebuild the image from time to time to keep freqtrade (and dependencies) up-to-date. ``` bash docker-compose -f docker/docker-compose-jupyter.yml build --no-cache From e69dac27043368af4e0cd0b81c21ca494adf2bd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Jan 2021 15:38:46 +0100 Subject: [PATCH 144/563] Fix bug in RPC history mode when no data is found --- freqtrade/rpc/rpc.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4ad2b3485..19c90fff0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -788,6 +788,8 @@ class RPC: timerange=timerange_parsed, data_format=config.get('dataformat_ohlcv', 'json'), ) + if pair not in _data: + raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.") from freqtrade.resolvers.strategy_resolver import StrategyResolver strategy = StrategyResolver.load_strategy(config) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f6e0ccd76..dfb0fb956 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1046,6 +1046,14 @@ def test_api_pair_history(botclient, ohlcv_history): assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00' assert rc.json()['data_stop_ts'] == 1515715200000 + # No data found + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" + "&timerange=20200111-20200112&strategy=DefaultStrategy") + assert_response(rc, 502) + assert rc.json()['error'] == ("Error querying /api/v1/pair_history: " + "No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") + def test_api_plot_config(botclient): ftbot, client = botclient From c9e477214fddc6f60a08c3914c0fdd33ffcf9ecc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Jan 2021 16:37:09 +0100 Subject: [PATCH 145/563] Allow protections to be set in the strategy --- docs/configuration.md | 3 +- docs/includes/protections.md | 47 +++++++++++++++++++++++- freqtrade/resolvers/strategy_resolver.py | 1 + freqtrade/strategy/interface.py | 3 ++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index b70a85c04..13c7eb47b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -91,7 +91,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts -| `protections` | Define one or more protections to be used. [More information below](#protections).
**Datatype:** List of Dicts +| `protections` | Define one or more protections to be used. [More information below](#protections). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** List of Dicts | `telegram.enabled` | Enable the usage of Telegram.
**Datatype:** Boolean | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String @@ -141,6 +141,7 @@ Values set in the configuration file always overwrite values set in the strategy * `stake_amount` * `unfilledtimeout` * `disable_dataframe_checks` +* `protections` * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index b98a0d662..8465392a4 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -7,7 +7,8 @@ Protections will protect your strategy from unexpected events and market conditi All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys. !!! Note - Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance. + Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance. + To align your protection with your strategy, you can define protections in the strategy. !!! Tip Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). @@ -167,3 +168,47 @@ The below example assumes a timeframe of 1 hour: } ], ``` + +You can use the same in your strategy, the syntax is only slightly different: + +``` python +from freqtrade.strategy import IStrategy + +class AwesomeStrategy(IStrategy) + timeframe = '1h' + protections = [ + { + "method": "CooldownPeriod", + "stop_duration_candles": 5 + }, + { + "method": "MaxDrawdown", + "lookback_period_candles": 48, + "trade_limit": 20, + "stop_duration_candles": 4, + "max_allowed_drawdown": 0.2 + }, + { + "method": "StoplossGuard", + "lookback_period_candles": 24, + "trade_limit": 4, + "stop_duration_candles": 2, + "only_per_pair": False + }, + { + "method": "LowProfitPairs", + "lookback_period_candles": 6, + "trade_limit": 2, + "stop_duration_candles": 60, + "required_profit": 0.02 + }, + { + "method": "LowProfitPairs", + "lookback_period_candles": 24, + "trade_limit": 4, + "stop_duration_candles": 2, + "required_profit": 0.01 + } + ] + # ... +``` diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 73af00fee..2b7a4f0c2 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -73,6 +73,7 @@ class StrategyResolver(IResolver): ("order_time_in_force", None, None), ("stake_currency", None, None), ("stake_amount", None, None), + ("protections", None, None), ("startup_candle_count", None, None), ("unfilledtimeout", None, None), ("use_sell_signal", True, 'ask_strategy'), diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1e4fc8b12..348e6a446 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -119,6 +119,9 @@ class IStrategy(ABC): # Count of candles the strategy requires before producing valid signals startup_candle_count: int = 0 + # Protections + protections: List + # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. From b43ef474ad231c1ac9cf87c760107ba7a5292a04 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 7 Jan 2021 07:51:49 +0100 Subject: [PATCH 146/563] Fix expired candle implementation Improve and simplify test by passing the current time to the function --- freqtrade/strategy/interface.py | 8 ++++---- tests/strategy/test_interface.py | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a18c6c915..b9d05f64f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -483,16 +483,16 @@ class IStrategy(ABC): latest['date'], pair, str(buy), str(sell)) timeframe_seconds = timeframe_to_seconds(timeframe) if self.ignore_expired_candle(latest_date=latest_date, + current_time=datetime.now(timezone.utc), timeframe_seconds=timeframe_seconds, buy=buy): return False, sell return buy, sell - def ignore_expired_candle(self, latest_date: datetime, timeframe_seconds: int, buy: bool): + def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, + timeframe_seconds: int, buy: bool): if self.ignore_buying_expired_candle and buy: - current_time = datetime.now(timezone.utc) - timedelta( - seconds=self.ignore_buying_expired_candle_after) - time_delta = current_time - latest_date + timedelta(seconds=timeframe_seconds) + time_delta = current_time - (latest_date + timedelta(seconds=timeframe_seconds)) return time_delta.total_seconds() > self.ignore_buying_expired_candle_after else: return False diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index d7d113a4e..a3969d91b 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -106,21 +106,28 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) -def test_ignore_expired_candle(default_conf, ohlcv_history): +def test_ignore_expired_candle(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.ignore_buying_expired_candle = True strategy.ignore_buying_expired_candle_after = 60 - ohlcv_history.loc[-1, 'date'] = arrow.utcnow() - # Take a copy to correctly modify the call - mocked_history = ohlcv_history.copy() - latest_date = mocked_history['date'].max() + latest_date = datetime(2020, 12, 30, 7, 0, 0, tzinfo=timezone.utc) + # Add 1 candle length as the "latest date" defines candle open. + current_time = latest_date + timedelta(seconds=80 + 300) assert strategy.ignore_expired_candle(latest_date=latest_date, + current_time=current_time, timeframe_seconds=300, buy=True) is True + current_time = latest_date + timedelta(seconds=30 + 300) + + assert not strategy.ignore_expired_candle(latest_date=latest_date, + current_time=current_time, + timeframe_seconds=300, + buy=True) is True + def test_assert_df_raise(mocker, caplog, ohlcv_history): ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) From 9e66417e852783a4a1da4df0e11deae4d16fee86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 7 Jan 2021 19:21:42 +0100 Subject: [PATCH 147/563] Run CI for mac on 3.9 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index daa10fea7..7b0418a11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,7 +117,7 @@ jobs: strategy: matrix: os: [ macos-latest ] - python-version: [3.7, 3.8] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 @@ -146,7 +146,7 @@ jobs: run: | cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. - - name: Installation - *nix + - name: Installation - macOS run: | python -m pip install --upgrade pip export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH From 54ab61d18a717a3e00b57ae8acdbc60d698f37f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 7 Jan 2021 19:27:35 +0100 Subject: [PATCH 148/563] Install hdf5 via brew --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b0418a11..41ac0770d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,6 +148,7 @@ jobs: - name: Installation - macOS run: | + brew install hdf5 python -m pip install --upgrade pip export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib From 124cb5c5bff02a536da3529e05742fa353a87cb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 7 Jan 2021 19:36:50 +0100 Subject: [PATCH 149/563] Add cblosc brew dependency --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41ac0770d..961dfef71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,7 +148,7 @@ jobs: - name: Installation - macOS run: | - brew install hdf5 + brew install hdf5 c-blosc python -m pip install --upgrade pip export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib From bf182dc01e6187f81d3bd61dfcb4a43e754e1c5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 7 Jan 2021 20:03:34 +0100 Subject: [PATCH 150/563] Fix wrong key usage in trade_history_timebased --- freqtrade/exchange/exchange.py | 2 +- tests/conftest.py | 10 +++++----- tests/exchange/test_exchange.py | 25 +++++++++++++------------ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 11a0ef8e6..b610b28f4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -936,7 +936,7 @@ class Exchange: while True: t = await self._async_fetch_trades(pair, since=since) if len(t): - since = t[-1][1] + since = t[-1][0] trades.extend(t) # Reached the end of the defined-download period if until and t[-1][0] > until: diff --git a/tests/conftest.py b/tests/conftest.py index 9eda0e973..75a98dcc5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1492,11 +1492,11 @@ def trades_for_order(): @pytest.fixture(scope="function") def trades_history(): - return [[1565798389463, '126181329', None, 'buy', 0.019627, 0.04, 0.00078508], - [1565798399629, '126181330', None, 'buy', 0.019627, 0.244, 0.004788987999999999], - [1565798399752, '126181331', None, 'sell', 0.019626, 0.011, 0.00021588599999999999], - [1565798399862, '126181332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999], - [1565798399872, '126181333', None, 'sell', 0.019626, 0.011, 0.00021588599999999999]] + return [[1565798389463, '12618132aa9', None, 'buy', 0.019627, 0.04, 0.00078508], + [1565798399629, '1261813bb30', None, 'buy', 0.019627, 0.244, 0.004788987999999999], + [1565798399752, '1261813cc31', None, 'sell', 0.019626, 0.011, 0.00021588599999999999], + [1565798399862, '126181cc332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999], + [1565798399872, '1261aa81333', None, 'sell', 0.019626, 0.011, 0.00021588599999999999]] @pytest.fixture(scope="function") diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a42ff52e4..f33382da4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1759,36 +1759,37 @@ async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchang @pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_get_trade_history_time(default_conf, mocker, caplog, exchange_name, - trades_history): + fetch_trades_result): caplog.set_level(logging.DEBUG) async def mock_get_trade_hist(pair, *args, **kwargs): - if kwargs['since'] == trades_history[0][0]: - return trades_history[:-1] + if kwargs['since'] == fetch_trades_result[0]['timestamp']: + return fetch_trades_result[:-1] else: - return trades_history[-1:] + return fetch_trades_result[-1:] caplog.set_level(logging.DEBUG) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) # Monkey-patch async function - exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist) + exchange._api_async.fetch_trades = MagicMock(side_effect=mock_get_trade_hist) pair = 'ETH/BTC' - ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0][0], - until=trades_history[-1][0]-1) + ret = await exchange._async_get_trade_history_time(pair, + since=fetch_trades_result[0]['timestamp'], + until=fetch_trades_result[-1]['timestamp']-1) assert type(ret) is tuple assert ret[0] == pair assert type(ret[1]) is list - assert len(ret[1]) == len(trades_history) - assert exchange._async_fetch_trades.call_count == 2 - fetch_trades_cal = exchange._async_fetch_trades.call_args_list + assert len(ret[1]) == len(fetch_trades_result) + assert exchange._api_async.fetch_trades.call_count == 2 + fetch_trades_cal = exchange._api_async.fetch_trades.call_args_list # first call (using since, not fromId) assert fetch_trades_cal[0][0][0] == pair - assert fetch_trades_cal[0][1]['since'] == trades_history[0][0] + assert fetch_trades_cal[0][1]['since'] == fetch_trades_result[0]['timestamp'] # 2nd call assert fetch_trades_cal[1][0][0] == pair - assert fetch_trades_cal[0][1]['since'] == trades_history[0][0] + assert fetch_trades_cal[1][1]['since'] == fetch_trades_result[-2]['timestamp'] assert log_has_re(r"Stopping because until was reached.*", caplog) From 4f126bea3582242155f412a5e4175b8cfe363694 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 7 Jan 2021 20:06:26 +0100 Subject: [PATCH 151/563] Change trades-test2 to better test correct behaviour --- tests/exchange/test_exchange.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index f33382da4..7d9954cb9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1718,8 +1718,8 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, @pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) -async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchange_name, - trades_history): +async def test__async_get_trade_history_id(default_conf, mocker, exchange_name, + fetch_trades_result): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) pagination_arg = exchange._trades_pagination_arg @@ -1727,28 +1727,29 @@ async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchang async def mock_get_trade_hist(pair, *args, **kwargs): if 'since' in kwargs: # Return first 3 - return trades_history[:-2] - elif kwargs.get('params', {}).get(pagination_arg) == trades_history[-3][1]: + return fetch_trades_result[:-2] + elif kwargs.get('params', {}).get(pagination_arg) == fetch_trades_result[-3]['id']: # Return 2 - return trades_history[-3:-1] + return fetch_trades_result[-3:-1] else: # Return last 2 - return trades_history[-2:] + return fetch_trades_result[-2:] # Monkey-patch async function - exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist) + exchange._api_async.fetch_trades = MagicMock(side_effect=mock_get_trade_hist) pair = 'ETH/BTC' - ret = await exchange._async_get_trade_history_id(pair, since=trades_history[0][0], - until=trades_history[-1][0]-1) + ret = await exchange._async_get_trade_history_id(pair, + since=fetch_trades_result[0]['timestamp'], + until=fetch_trades_result[-1]['timestamp'] - 1) assert type(ret) is tuple assert ret[0] == pair assert type(ret[1]) is list - assert len(ret[1]) == len(trades_history) - assert exchange._async_fetch_trades.call_count == 3 - fetch_trades_cal = exchange._async_fetch_trades.call_args_list + assert len(ret[1]) == len(fetch_trades_result) + assert exchange._api_async.fetch_trades.call_count == 3 + fetch_trades_cal = exchange._api_async.fetch_trades.call_args_list # first call (using since, not fromId) assert fetch_trades_cal[0][0][0] == pair - assert fetch_trades_cal[0][1]['since'] == trades_history[0][0] + assert fetch_trades_cal[0][1]['since'] == fetch_trades_result[0]['timestamp'] # 2nd call assert fetch_trades_cal[1][0][0] == pair From 2e7faa782c7980d0a49d56ebb8c8d08399e0100c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 8 Jan 2021 06:51:37 +0100 Subject: [PATCH 152/563] Add documentation section for macOS installation error on 3.999999999 --- docs/installation.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/installation.md b/docs/installation.md index 73e791c56..a23399441 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -245,6 +245,19 @@ open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10 If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. +### MacOS installation error with python 3.9 + +When using python 3.9 on macOS, it's currently necessary to install some os-level modules to allow dependencies to compile. +The errors you'll see happen during installation and are related to the installation of `tables` or `blosc`. + +You can install the necessary libraries with the following command: + +``` bash +brew install hdf5 c-blosc +``` + +After this, please run the installation (script) again. + ----- Now you have an environment ready, the next step is From bd5f46e4c22fb0e8025e7c0bc0f5493e260c97d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Jan 2021 07:16:36 +0000 Subject: [PATCH 153/563] Bump pytest-mock from 3.4.0 to 3.5.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.4.0 to 3.5.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.4.0...v3.5.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e918a9b90..3c037b7cd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.790 pytest==6.2.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 -pytest-mock==3.4.0 +pytest-mock==3.5.0 pytest-random-order==1.0.4 isort==5.7.0 From f3319e1382d26c1685339e2def3419435ec2b9eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Jan 2021 07:16:48 +0000 Subject: [PATCH 154/563] Bump prompt-toolkit from 3.0.8 to 3.0.9 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.8 to 3.0.9. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/commits/3.0.9) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5099491dd..e7d8398eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,4 +36,4 @@ pyjwt==1.7.1 colorama==0.4.4 # Building config files interactively questionary==1.9.0 -prompt-toolkit==3.0.8 +prompt-toolkit==3.0.9 From 784630e2f2a215f7e811e0e393564891f6d45233 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Jan 2021 07:16:49 +0000 Subject: [PATCH 155/563] Bump uvicorn from 0.13.2 to 0.13.3 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.13.2 to 0.13.3. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.13.2...0.13.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5099491dd..44fadce73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ sdnotify==0.3.2 # API Server fastapi==0.63.0 -uvicorn==0.13.2 +uvicorn==0.13.3 pyjwt==1.7.1 # Support for colorized terminal output From 3cf506fa5d2b9dc8f764b83fbf3921fdea508af1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Jan 2021 07:16:49 +0000 Subject: [PATCH 156/563] Bump ccxt from 1.40.14 to 1.40.25 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.40.14 to 1.40.25. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.40.14...1.40.25) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5099491dd..c8d2d0d54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.2.0 -ccxt==1.40.14 +ccxt==1.40.25 aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 From 4d2c59b7ecbb9b8431da1c922614480cc91c91b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Jan 2021 07:47:47 +0000 Subject: [PATCH 157/563] Bump numpy from 1.19.4 to 1.19.5 Bumps [numpy](https://github.com/numpy/numpy) from 1.19.4 to 1.19.5. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.19.4...v1.19.5) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 87ec29fb6..b564a4244 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.19.4 +numpy==1.19.5 pandas==1.2.0 ccxt==1.40.25 From c8df3c4730dacd955ed2bb207fd02b099b4c007e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Jan 2021 07:48:31 +0000 Subject: [PATCH 158/563] Bump pyjwt from 1.7.1 to 2.0.0 Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 1.7.1 to 2.0.0. - [Release notes](https://github.com/jpadilla/pyjwt/releases) - [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.md) - [Commits](https://github.com/jpadilla/pyjwt/compare/1.7.1...2.0.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 87ec29fb6..ae6ba2e46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ sdnotify==0.3.2 # API Server fastapi==0.63.0 uvicorn==0.13.3 -pyjwt==1.7.1 +pyjwt==2.0.0 # Support for colorized terminal output colorama==0.4.4 From 378a252ad1d3a40e531ce1dcadf6ac8d4fcb1653 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 8 Jan 2021 13:46:43 +0100 Subject: [PATCH 159/563] Fix #4161 - by not using the problematic method for windows --- tests/optimize/test_optimize_reports.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index a0e1932ff..e194e7de4 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,5 +1,5 @@ import re -from datetime import timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path import pandas as pd @@ -121,8 +121,8 @@ def test_generate_backtest_stats(default_conf, testdatadir): } assert strat_stats['max_drawdown'] == 0.0 - assert strat_stats['drawdown_start'] == Arrow.fromtimestamp(0).datetime - assert strat_stats['drawdown_end'] == Arrow.fromtimestamp(0).datetime + assert strat_stats['drawdown_start'] == datetime(1970, 1, 1, tzinfo=timezone.utc) + assert strat_stats['drawdown_end'] == datetime(1970, 1, 1, tzinfo=timezone.utc) assert strat_stats['drawdown_end_ts'] == 0 assert strat_stats['drawdown_start_ts'] == 0 assert strat_stats['pairlist'] == ['UNITTEST/BTC'] From 8631a54514084879c9507fe7d0f89ae4464d84b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 8 Jan 2021 19:27:51 +0100 Subject: [PATCH 160/563] Fix test due to pyjwt2.0 --- freqtrade/rpc/api_server/api_auth.py | 2 +- tests/rpc/test_rpc_apiserver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_auth.py b/freqtrade/rpc/api_server/api_auth.py index 110bb2a25..a39e31b85 100644 --- a/freqtrade/rpc/api_server/api_auth.py +++ b/freqtrade/rpc/api_server/api_auth.py @@ -44,7 +44,7 @@ def get_user_from_token(token, secret_key: str, token_type: str = "access"): return username -def create_token(data: dict, secret_key: str, token_type: str = "access") -> bytes: +def create_token(data: dict, secret_key: str, token_type: str = "access") -> str: to_encode = data.copy() if token_type == "access": expire = datetime.utcnow() + timedelta(minutes=15) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index dfb0fb956..2212d4a79 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -93,7 +93,7 @@ def test_api_auth(): create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234', token_type="NotATokenType") token = create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234') - assert isinstance(token, bytes) + assert isinstance(token, str) u = get_user_from_token(token, 'secret1234') assert u == 'Freqtrade' From ddecf3ef98b4c06a056effa319eaf8097d0ce190 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Jan 2021 05:38:34 +0000 Subject: [PATCH 161/563] Bump mkdocs-material from 6.2.3 to 6.2.4 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.2.3 to 6.2.4. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.2.3...6.2.4) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 2db336f4a..adf4bc96c 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.2.3 +mkdocs-material==6.2.4 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1 From a34753fcb16db610b7fcf3717d372c35b0d743c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Jan 2021 05:38:45 +0000 Subject: [PATCH 162/563] Bump prompt-toolkit from 3.0.9 to 3.0.10 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.9 to 3.0.10. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.9...3.0.10) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1d0d57091..69081108a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,4 +36,4 @@ pyjwt==2.0.0 colorama==0.4.4 # Building config files interactively questionary==1.9.0 -prompt-toolkit==3.0.9 +prompt-toolkit==3.0.10 From f1809286cfefd216a43ceac6e35d113f22b26a1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Jan 2021 05:38:47 +0000 Subject: [PATCH 163/563] Bump ccxt from 1.40.25 to 1.40.30 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.40.25 to 1.40.30. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.40.25...1.40.30) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1d0d57091..fc3fb1a00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.5 pandas==1.2.0 -ccxt==1.40.25 +ccxt==1.40.30 aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 From 59efc5f083050bc34e3ed5cdf51004b286d92124 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Jan 2021 05:38:47 +0000 Subject: [PATCH 164/563] Bump pytest-mock from 3.5.0 to 3.5.1 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.5.0 to 3.5.1. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.5.0...v3.5.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3c037b7cd..883c3089f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.790 pytest==6.2.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 -pytest-mock==3.5.0 +pytest-mock==3.5.1 pytest-random-order==1.0.4 isort==5.7.0 From f159c464380ba12d283cd1bddddc661a3cff3b32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jan 2021 07:55:01 +0100 Subject: [PATCH 165/563] Include stoploss_on_exchange in stoploss_guard fix #4183 --- docs/includes/protections.md | 2 +- freqtrade/plugins/protections/stoploss_guard.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 8465392a4..95fa53ad2 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -58,7 +58,7 @@ The below example stops trading for all pairs for 4 candles after the last trade ``` !!! Note - `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the resulting profit was negative. + `StoplossGuard` considers all trades with the results `"stop_loss"`, `"stoploss_on_exchange"` and `"trailing_stop_loss"` if the resulting profit was negative. `trade_limit` and `lookback_period` will need to be tuned for your strategy. #### MaxDrawdown diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 193907ddc..92fae54cb 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -53,8 +53,9 @@ class StoplossGuard(IProtection): # trades = Trade.get_trades(filters).all() trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) - trades = [trade for trade in trades1 if str(trade.sell_reason) == SellType.STOP_LOSS.value - or (str(trade.sell_reason) == SellType.TRAILING_STOP_LOSS.value + trades = [trade for trade in trades1 if (str(trade.sell_reason) in ( + SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, + SellType.STOPLOSS_ON_EXCHANGE.value) and trade.close_profit < 0)] if len(trades) > self._trade_limit: From dbc25f00acdded97725d1e551ea1848665097e3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jan 2021 19:12:03 +0100 Subject: [PATCH 166/563] Switch full config from bittrex to binance bittrex no longer supports volumepairlist. closes #4192 --- config_full.json.example | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 7cdd6af67..db8debb2c 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -103,7 +103,7 @@ } ], "exchange": { - "name": "bittrex", + "name": "binance", "sandbox": false, "key": "your_exchange_key", "secret": "your_exchange_secret", @@ -115,16 +115,21 @@ "aiohttp_trust_env": false }, "pair_whitelist": [ + "ALGO/BTC", + "ATOM/BTC", + "BAT/BTC", + "BCH/BTC", + "BRD/BTC", + "EOS/BTC", "ETH/BTC", + "IOTA/BTC", + "LINK/BTC", "LTC/BTC", - "ETC/BTC", - "DASH/BTC", - "ZEC/BTC", - "XLM/BTC", - "NXT/BTC", - "TRX/BTC", - "ADA/BTC", - "XMR/BTC" + "NEO/BTC", + "NXS/BTC", + "XMR/BTC", + "XRP/BTC", + "XTZ/BTC" ], "pair_blacklist": [ "DOGE/BTC" @@ -147,7 +152,7 @@ "remove_pumps": false }, "telegram": { - "enabled": true, + "enabled": false, "token": "your_telegram_token", "chat_id": "your_telegram_chat_id", "notification_settings": { From 63a579dbab0eaafa0c699f01e6d266729575c5fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jan 2021 19:30:07 +0100 Subject: [PATCH 167/563] Add sell_profit_offset parameter Allows defining positive offsets before enabling the sell signal --- config_full.json.example | 1 + docs/configuration.md | 3 ++- freqtrade/constants.py | 1 + freqtrade/optimize/optimize_reports.py | 1 + freqtrade/resolvers/strategy_resolver.py | 1 + freqtrade/strategy/interface.py | 9 +++++---- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index db8debb2c..ef791d267 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -42,6 +42,7 @@ "order_book_max": 1, "use_sell_signal": true, "sell_profit_only": false, + "sell_profit_offset": 0.0, "ignore_roi_if_buy_signal": false }, "order_types": { diff --git a/docs/configuration.md b/docs/configuration.md index 13c7eb47b..926506f22 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -72,7 +72,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
**Datatype:** Positive Integer | `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
**Datatype:** Positive Integer | `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
**Datatype:** Boolean -| `ask_strategy.sell_profit_only` | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean +| `ask_strategy.sell_profit_only` | Wait until the bot reaches `ask_strategy.sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean +| `ask_strategy.sell_profit_offset` | Sell-signal is only active above this value. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0`.*
**Datatype:** Float (as ratio) | `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e7d7e80f6..d48ab635e 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -154,6 +154,7 @@ CONF_SCHEMA = { 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'use_sell_signal': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'}, + 'sell_profit_offset': {'type': 'number', 'minimum': 0.0}, 'ignore_roi_if_buy_signal': {'type': 'boolean'} } }, diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d029ecd13..6c70b7c84 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -299,6 +299,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'minimal_roi': config['minimal_roi'], 'use_sell_signal': config['ask_strategy']['use_sell_signal'], 'sell_profit_only': config['ask_strategy']['sell_profit_only'], + 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], **daily_stats, } diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 2b7a4f0c2..825e6572a 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -79,6 +79,7 @@ class StrategyResolver(IResolver): ("use_sell_signal", True, 'ask_strategy'), ("sell_profit_only", False, 'ask_strategy'), ("ignore_roi_if_buy_signal", False, 'ask_strategy'), + ("sell_profit_offset", 0.0, 'ask_strategy'), ("disable_dataframe_checks", False, None), ] for attribute, default, subkey in attributes: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 348e6a446..8546b1eda 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -505,18 +505,19 @@ class IStrategy(ABC): # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_ratio(current_rate) - config_ask_strategy = self.config.get('ask_strategy', {}) + ask_strategy = self.config.get('ask_strategy', {}) # if buy signal and ignore_roi is set, we don't need to evaluate min_roi. - roi_reached = (not (buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False)) + roi_reached = (not (buy and ask_strategy.get('ignore_roi_if_buy_signal', False)) and self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date)) - if config_ask_strategy.get('sell_profit_only', False) and trade.calc_profit(rate=rate) <= 0: + if (ask_strategy.get('sell_profit_only', False) + and trade.calc_profit(rate=rate) <= ask_strategy.get('sell_profit_offset', 0)): # Negative profits and sell_profit_only - ignore sell signal sell_signal = False else: - sell_signal = sell and not buy and config_ask_strategy.get('use_sell_signal', True) + sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True) # TODO: return here if sell-signal should be favored over ROI # Start evaluations From b062b836cc6e5c12e3eebf964d191fe70e1d04ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jan 2021 19:42:44 +0100 Subject: [PATCH 168/563] Add test for sell_profit_offset --- tests/test_freqtradebot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5c5666788..e6aff3352 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3065,6 +3065,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy default_conf['ask_strategy'] = { 'use_sell_signal': True, 'sell_profit_only': True, + 'sell_profit_offset': 0.1, } freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -3076,7 +3077,11 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy trade.update(limit_buy_order) freqtrade.wallets.update() patch_get_signal(freqtrade, value=(False, True)) + assert freqtrade.handle_trade(trade) is False + + freqtrade.config['ask_strategy']['sell_profit_offset'] = 0.0 assert freqtrade.handle_trade(trade) is True + assert trade.sell_reason == SellType.SELL_SIGNAL.value From 4d7ffa8c810e20b95ca746e61b5e7f2e964ac5ad Mon Sep 17 00:00:00 2001 From: nas- Date: Tue, 12 Jan 2021 01:13:58 +0100 Subject: [PATCH 169/563] Added suppoort for regex in whitelist --- freqtrade/commands/data_commands.py | 17 +++--- freqtrade/edge/edge_positioning.py | 7 ++- freqtrade/exchange/exchange.py | 4 +- freqtrade/plot/plotting.py | 14 +++-- freqtrade/plugins/pairlist/IPairList.py | 9 +++ freqtrade/plugins/pairlist/StaticPairList.py | 5 +- freqtrade/plugins/pairlistmanager.py | 20 +++++++ tests/exchange/test_exchange.py | 63 ++++++++++---------- tests/test_plotting.py | 5 +- 9 files changed, 93 insertions(+), 51 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 25c7d0436..1ce02eee5 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -10,6 +10,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_oh refresh_backtest_trades_data) from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes +from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -42,15 +43,17 @@ def start_download_data(args: Dict[str, Any]) -> None: "Downloading data requires a list of pairs. " "Please check the documentation on how to configure this.") - logger.info(f"About to download pairs: {config['pairs']}, " - f"intervals: {config['timeframes']} to {config['datadir']}") - pairs_not_available: List[str] = [] # Init exchange exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) # Manual validations of relevant settings exchange.validate_pairs(config['pairs']) + expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets)) + + logger.info(f"About to download pairs: {expanded_pairs}, " + f"intervals: {config['timeframes']} to {config['datadir']}") + for timeframe in config['timeframes']: exchange.validate_timeframes(timeframe) @@ -58,20 +61,20 @@ def start_download_data(args: Dict[str, Any]) -> None: if config.get('download_trades'): pairs_not_available = refresh_backtest_trades_data( - exchange, pairs=config['pairs'], datadir=config['datadir'], + exchange, pairs=expanded_pairs, datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), data_format=config['dataformat_trades']) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( - pairs=config['pairs'], timeframes=config['timeframes'], + pairs=expanded_pairs, timeframes=config['timeframes'], datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), data_format_ohlcv=config['dataformat_ohlcv'], data_format_trades=config['dataformat_trades'], - ) + ) else: pairs_not_available = refresh_backtest_ohlcv_data( - exchange, pairs=config['pairs'], timeframes=config['timeframes'], + exchange, pairs=expanded_pairs, timeframes=config['timeframes'], datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv']) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 037717c68..e549a3701 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -12,6 +12,7 @@ from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.data.history import get_timerange, load_data, refresh_data from freqtrade.exceptions import OperationalException +from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.strategy.interface import SellType @@ -80,10 +81,12 @@ class Edge: if config.get('fee'): self.fee = config['fee'] else: - self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) + self.fee = self.exchange.get_fee(symbol=expand_pairlist( + self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0]) def calculate(self) -> bool: - pairs = self.config['exchange']['pair_whitelist'] + pairs = expand_pairlist(self.config['exchange']['pair_whitelist'], + list(self.exchange.markets)) heartbeat = self.edge_config.get('process_throttle_secs') if (self._last_updated > 0) and ( diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b610b28f4..489f70528 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -25,6 +25,7 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier, retrier_async) from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 +from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist CcxtModuleType = Any @@ -335,8 +336,9 @@ class Exchange: if not self.markets: logger.warning('Unable to validate pairs (assuming they are correct).') return + extended_pairs = expand_pairlist(pairs, list(self.markets)) invalid_pairs = [] - for pair in pairs: + for pair in extended_pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs # TODO: add a support for having coins in BTC/USDT format if self.markets and pair not in self.markets: diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 40e3da9c9..996c5276c 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -13,6 +13,7 @@ from freqtrade.data.history import get_timerange, load_data from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds from freqtrade.misc import pair_to_filename +from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy import IStrategy @@ -29,16 +30,16 @@ except ImportError: exit(1) -def init_plotscript(config, startup_candles: int = 0): +def init_plotscript(config, markets: List, startup_candles: int = 0): """ Initialize objects needed for plotting :return: Dict with candle (OHLCV) data, trades and pairs """ if "pairs" in config: - pairs = config['pairs'] + pairs = expand_pairlist(config['pairs'], markets) else: - pairs = config['exchange']['pair_whitelist'] + pairs = expand_pairlist(config['exchange']['pair_whitelist'], markets) # Set timerange to use timerange = TimeRange.parse_timerange(config.get('timerange')) @@ -177,7 +178,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: trades['desc'] = trades.apply(lambda row: f"{round(row['profit_percent'] * 100, 1)}%, " f"{row['sell_reason']}, " f"{row['trade_duration']} min", - axis=1) + axis=1) trade_buys = go.Scatter( x=trades["open_date"], y=trades["open_rate"], @@ -527,7 +528,7 @@ def load_and_plot_trades(config: Dict[str, Any]): exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) IStrategy.dp = DataProvider(config, exchange) - plot_elements = init_plotscript(config, strategy.startup_candle_count) + plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count) timerange = plot_elements['timerange'] trades = plot_elements['trades'] pair_counter = 0 @@ -562,7 +563,8 @@ def plot_profit(config: Dict[str, Any]) -> None: But should be somewhat proportional, and therefor useful in helping out to find a good algorithm. """ - plot_elements = init_plotscript(config) + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) + plot_elements = init_plotscript(config, list(exchange.markets)) trades = plot_elements['trades'] # Filter trades to relevant pairs # Remove open pairs - we don't know the profit yet so can't calculate profit for these. diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 865aa90d6..f9809aeb1 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -124,6 +124,15 @@ class IPairList(LoggingMixin, ABC): """ return self._pairlistmanager.verify_blacklist(pairlist, logmethod) + def verify_whitelist(self, pairlist: List[str], logmethod) -> List[str]: + """ + Proxy method to verify_whitelist for easy access for child classes. + :param pairlist: Pairlist to validate + :param logmethod: Function that'll be called, `logger.info` or `logger.warning`. + :return: pairlist - whitelisted pairs + """ + return self._pairlistmanager.verify_whitelist(pairlist, logmethod) + def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]: """ Check available markets and remove pair from whitelist if necessary diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index dd592e0ca..c216a322d 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -50,9 +50,10 @@ class StaticPairList(IPairList): :return: List of pairs """ if self._allow_inactive: - return self._config['exchange']['pair_whitelist'] + return self.verify_whitelist(self._config['exchange']['pair_whitelist'], logger.info) else: - return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist']) + return self._whitelist_for_active_markets( + self.verify_whitelist(self._config['exchange']['pair_whitelist'], logger.info)) def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index ad7b46cb8..c471d7c51 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -59,6 +59,11 @@ class PairListManager(): """The expanded blacklist (including wildcard expansion)""" return expand_pairlist(self._blacklist, self._exchange.get_markets().keys()) + @property + def expanded_whitelist(self) -> List[str]: + """The expanded whitelist (including wildcard expansion)""" + return expand_pairlist(self._whitelist, self._exchange.get_markets().keys()) + @property def name_list(self) -> List[str]: """Get list of loaded Pairlist Handler names""" @@ -129,6 +134,21 @@ class PairListManager(): pairlist.remove(pair) return pairlist + def verify_whitelist(self, pairlist: List[str], logmethod) -> List[str]: + """ + Verify and remove items from pairlist - returning a filtered pairlist. + Logs a warning or info depending on `aswarning`. + Pairlist Handlers explicitly using this method shall use + `logmethod=logger.info` to avoid spamming with warning messages + :return: pairlist - blacklisted pairs + """ + try: + whitelist = self.expanded_whitelist + except ValueError as err: + logger.error(f"Pair blacklist contains an invalid Wildcard: {err}") + return [] + return whitelist + def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes: """ Create list of pair tuples with (pair, timeframe) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7d9954cb9..8cd2a9bca 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -505,37 +505,38 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d Exchange(default_conf) -def test_validate_pairs_not_available(default_conf, mocker): - api_mock = MagicMock() - type(api_mock).markets = PropertyMock(return_value={ - 'XRP/BTC': {'inactive': True} - }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - - with pytest.raises(OperationalException, match=r'not available'): - Exchange(default_conf) - - -def test_validate_pairs_exception(default_conf, mocker, caplog): - caplog.set_level(logging.INFO) - api_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) - - type(api_mock).markets = PropertyMock(return_value={}) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - - with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): - Exchange(default_conf) - - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})) - Exchange(default_conf) - assert log_has('Unable to validate pairs (assuming they are correct).', caplog) +# This cannot happen anymore as expand_pairlist implicitly filters out unavaliablie pairs +# def test_validate_pairs_not_available(default_conf, mocker): +# api_mock = MagicMock() +# type(api_mock).markets = PropertyMock(return_value={ +# 'XRP/BTC': {'inactive': True, 'base': 'XRP', 'quote': 'BTC'} +# }) +# mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) +# mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') +# mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') +# mocker.patch('freqtrade.exchange.Exchange._load_async_markets') +# +# with pytest.raises(OperationalException, match=r'not available'): +# Exchange(default_conf) +# +# +# def test_validate_pairs_exception(default_conf, mocker, caplog): +# caplog.set_level(logging.INFO) +# api_mock = MagicMock() +# mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) +# +# type(api_mock).markets = PropertyMock(return_value={}) +# mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) +# mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') +# mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') +# mocker.patch('freqtrade.exchange.Exchange._load_async_markets') +# +# with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): +# Exchange(default_conf) +# +# mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})) +# Exchange(default_conf) +# assert log_has('Unable to validate pairs (assuming they are correct).', caplog) def test_validate_pairs_restricted(default_conf, mocker, caplog): diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 8e7b0ef7c..8f3ac464e 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -47,14 +47,15 @@ def test_init_plotscript(default_conf, mocker, testdatadir): default_conf['timeframe'] = "5m" default_conf["datadir"] = testdatadir default_conf['exportfilename'] = testdatadir / "backtest-result_test.json" - ret = init_plotscript(default_conf) + supported_markets = ["TRX/BTC", "ADA/BTC"] + ret = init_plotscript(default_conf, supported_markets) assert "ohlcv" in ret assert "trades" in ret assert "pairs" in ret assert 'timerange' in ret default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"] - ret = init_plotscript(default_conf, 20) + ret = init_plotscript(default_conf, supported_markets, 20) assert "ohlcv" in ret assert "TRX/BTC" in ret["ohlcv"] assert "ADA/BTC" in ret["ohlcv"] From e328182bd7d3bd40f850bb26179f97e868786ada Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Tue, 12 Jan 2021 07:30:39 +0100 Subject: [PATCH 170/563] Changed workings so it only needs to timing-parameter, instead of also requiring a boolean value --- docs/configuration.md | 7 ++----- freqtrade/resolvers/strategy_resolver.py | 1 - freqtrade/strategy/interface.py | 8 ++++---- tests/strategy/test_interface.py | 1 - 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 6ae37e7b3..93693c919 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -74,8 +74,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
**Datatype:** Boolean | `ask_strategy.sell_profit_only` | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean -| `ask_strategy.ignore_buying_expired_candle` | Enables usage of skipping buys on candles that are older than a specified period.
*Defaults to `False`*
**Datatype:** Boolean -| `ask_strategy.ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used when setting `ignore_buying_expired_candle`.
**Datatype:** Integer +| `ask_strategy.ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used.
**Datatype:** Integer | `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
**Datatype:** String @@ -146,7 +145,6 @@ Values set in the configuration file always overwrite values set in the strategy * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) -* `ignore_buying_expired_candle` (ask_strategy) * `ignore_buying_expired_candle_after` (ask_strategy) ### Configuring amount per trade @@ -679,13 +677,12 @@ freqtrade When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it. -In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle` to `true`. After this, you can set `ask_strategy.ignore_buying_expired_candle_after` to the number of seconds after which the candle becomes expired. +In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the candle becomes expired. For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy: ``` jsonc "ask_strategy":{ - "ignore_buying_expired_candle" = true "ignore_buying_expired_candle_after" = 300 # 5 minutes "price_side": "bid", // ... diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 5872d95a6..3b7374326 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -79,7 +79,6 @@ class StrategyResolver(IResolver): ("sell_profit_only", False, 'ask_strategy'), ("ignore_roi_if_buy_signal", False, 'ask_strategy'), ("disable_dataframe_checks", False, None), - ("ignore_buying_expired_candle", None, 'ask_strategy'), ("ignore_buying_expired_candle_after", 0, 'ask_strategy') ] for attribute, default, subkey in attributes: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b9d05f64f..57dcdeb3c 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -113,9 +113,7 @@ class IStrategy(ABC): # run "populate_indicators" only for new candle process_only_new_candles: bool = False - # Don't buy on expired candles - ignore_buying_expired_candle: bool = False - # Number of seconds after which the candle will no longer result in a buy + # Number of seconds after which the candle will no longer result in a buy on expired candles ignore_buying_expired_candle_after: int = 0 # Disable checking the dataframe (converts the error into a warning message) @@ -491,7 +489,9 @@ class IStrategy(ABC): def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, timeframe_seconds: int, buy: bool): - if self.ignore_buying_expired_candle and buy: + if self.ignore_buying_expired_candle_after \ + and self.ignore_buying_expired_candle_after > 0\ + and buy: time_delta = current_time - (latest_date + timedelta(seconds=timeframe_seconds)) return time_delta.total_seconds() > self.ignore_buying_expired_candle_after else: diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index a3969d91b..f158a1518 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -109,7 +109,6 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): def test_ignore_expired_candle(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) - strategy.ignore_buying_expired_candle = True strategy.ignore_buying_expired_candle_after = 60 latest_date = datetime(2020, 12, 30, 7, 0, 0, tzinfo=timezone.utc) From 71f45021b9083862b6d06fe7adf425a7de73ff49 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Tue, 12 Jan 2021 07:35:30 +0100 Subject: [PATCH 171/563] Removed redundant statement --- freqtrade/strategy/interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 57dcdeb3c..40debe78f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -490,7 +490,6 @@ class IStrategy(ABC): def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, timeframe_seconds: int, buy: bool): if self.ignore_buying_expired_candle_after \ - and self.ignore_buying_expired_candle_after > 0\ and buy: time_delta = current_time - (latest_date + timedelta(seconds=timeframe_seconds)) return time_delta.total_seconds() > self.ignore_buying_expired_candle_after From 1f6a71fdd96609a78ee9e75e232581e1d85f3776 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Tue, 12 Jan 2021 08:24:11 +0100 Subject: [PATCH 172/563] Reformat code on new version --- freqtrade/strategy/interface.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 40debe78f..2f1326f48 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -489,8 +489,7 @@ class IStrategy(ABC): def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, timeframe_seconds: int, buy: bool): - if self.ignore_buying_expired_candle_after \ - and buy: + if self.ignore_buying_expired_candle_after and buy: time_delta = current_time - (latest_date + timedelta(seconds=timeframe_seconds)) return time_delta.total_seconds() > self.ignore_buying_expired_candle_after else: From 60ea32e39811ea8c48bcab90fa201373731ba423 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Jan 2021 19:05:25 +0100 Subject: [PATCH 173/563] Improve wording --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5127ba5e0..25f1e7c46 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -679,7 +679,7 @@ freqtrade When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it. -In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the candle becomes expired. +In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the buy signal becomes expired. For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy: From ac43591c44bb0d90f21b1dbaa7c046bc2da3b9b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Jan 2021 19:24:37 +0100 Subject: [PATCH 174/563] Fix failing api when max_open_trades is unlimited --- freqtrade/rpc/rpc.py | 3 ++- tests/rpc/test_rpc_apiserver.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 19c90fff0..7c9be89aa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -649,7 +649,8 @@ class RPC: trades = Trade.get_open_trades() return { 'current': len(trades), - 'max': float(self._freqtrade.config['max_open_trades']), + 'max': (int(self._freqtrade.config['max_open_trades']) + if self._freqtrade.config['max_open_trades'] != float('inf') else -1), 'total_stake': sum((trade.open_rate * trade.amount) for trade in trades) } diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2212d4a79..4eb9a6fc8 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -364,14 +364,19 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert_response(rc) assert rc.json()["current"] == 0 - assert rc.json()["max"] == 1.0 + assert rc.json()["max"] == 1 # Create some test data ftbot.enter_positions() rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) - assert rc.json()["current"] == 1.0 - assert rc.json()["max"] == 1.0 + assert rc.json()["current"] == 1 + assert rc.json()["max"] == 1 + + ftbot.config['max_open_trades'] = float('inf') + rc = client_get(client, f"{BASE_URI}/count") + assert rc.json()["max"] == -1 + def test_api_locks(botclient): From 47a06c6213e9b885551dd9b35711dcf5fb77b90e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Jan 2021 19:27:49 +0100 Subject: [PATCH 175/563] Fix enable/reenable of swagger UI endpoint --- docs/rest-api.md | 2 +- freqtrade/rpc/api_server/webserver.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index a013bf358..2c7142c61 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -266,7 +266,7 @@ whitelist ## OpenAPI interface -To enable the builtin openAPI interface, specify `"enable_openapi": true` in the api_server configuration. +To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration. This will enable the Swagger UI at the `/docs` endpoint. By default, that's running at http://localhost:8080/docs/ - but it'll depend on your settings. ## Advanced API usage using JWT tokens diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 97dfa444d..9c0779274 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -30,8 +30,7 @@ class ApiServer(RPCHandler): api_config = self._config['api_server'] self.app = FastAPI(title="Freqtrade API", - openapi_url='openapi.json' if api_config.get( - 'enable_openapi', False) else None, + docs_url='/docs' if api_config.get('enable_openapi', False) else None, redoc_url=None, ) self.configure_app(self.app, self._config) From adb3fb123e0c5720a20c6ae3a2791d65592d22f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Jan 2021 19:35:02 +0100 Subject: [PATCH 176/563] Fix typo --- tests/rpc/test_rpc_apiserver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 4eb9a6fc8..5460519fe 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -378,7 +378,6 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json()["max"] == -1 - def test_api_locks(botclient): ftbot, client = botclient From 950c5c0113655128f06bbfc12929fd5ccb324bb8 Mon Sep 17 00:00:00 2001 From: tejeshreddy Date: Wed, 13 Jan 2021 16:09:03 +0530 Subject: [PATCH 177/563] fix: edge doc typos --- docs/edge.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index fd6d2cf7d..6f01fcf65 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -1,6 +1,6 @@ # Edge positioning -The `Edge Positioning` module uses probability to calculate your win rate and risk reward ration. It will use these statistics to control your strategy trade entry points, position side and, stoploss. +The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss. !!! Warning `Edge positioning` is not compatible with dynamic (volume-based) whitelist. @@ -55,7 +55,7 @@ Similarly, we can discover the set of losing trades $T_{lose}$ as follows: $$ T_{lose} = \{o \in O | o \leq 0\} $$ !!! Example - In a section where a strategy made three transactions $O = \{3.5, -1, 15, 0\}$:
+ In a section where a strategy made four transactions $O = \{3.5, -1, 15, 0\}$:
$T_{win} = \{3.5, 15\}$
$T_{lose} = \{-1, 0\}$
@@ -206,7 +206,7 @@ Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. - The strategy detects a sell signal in the **XLM/ETH** market. The bot exits **Trade 1** for a profit of $1$ **ETH**. The total capital in the wallet becomes $11$ **ETH** and the available capital for trading becomes $5.5$ **ETH**. -- **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**. +- **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2\%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**. ## Configurations From f3de0dd3eb8d55ef085258073d6506bd887f8cc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Jan 2021 06:53:40 +0100 Subject: [PATCH 178/563] Fix support for protections in hyperopt closes #4208 --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a689786ec..6913b2f4b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -180,6 +180,7 @@ class Backtesting: Backtesting setup method - called once for every call to "backtest()". """ PairLocks.use_db = False + PairLocks.timeframe = self.config['timeframe'] Trade.use_db = False if enable_protections: # Reset persisted data - used for protections only From 6d1fba140949740a129a75cae41d4adbed8a9745 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Sep 2020 07:43:42 +0200 Subject: [PATCH 179/563] Remove unnecessary log output tests --- tests/optimize/test_backtesting.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 376390664..1f8f7cfd8 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -353,8 +353,6 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: backtesting.start() # check the logs, that will contain the backtest result exists = [ - 'Using stake_currency: BTC ...', - 'Using stake_amount: 0.001 ...', 'Backtesting with data from 2017-11-14 21:17:00 ' 'up to 2017-11-14 22:59:00 (0 days)..' ] @@ -722,8 +720,6 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): '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-14 20:57:00 ' 'up to 2017-11-14 22:58:00 (0 days)..', 'Backtesting with data from 2017-11-14 21:17:00 ' @@ -786,8 +782,6 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): '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-14 20:57:00 ' 'up to 2017-11-14 22:58:00 (0 days)..', 'Backtesting with data from 2017-11-14 21:17:00 ' @@ -865,8 +859,6 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '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-14 20:57:00 ' 'up to 2017-11-14 22:58:00 (0 days)..', 'Backtesting with data from 2017-11-14 21:17:00 ' From 9d4cdcad10df21c149df59bdd7c4adde46553459 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Sep 2020 07:44:11 +0200 Subject: [PATCH 180/563] Extract backtesting of one strategy --- freqtrade/optimize/backtesting.py | 93 ++++++++++++++++--------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6913b2f4b..fff3914a5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -76,6 +76,8 @@ class Backtesting: # Reset keys for backtesting remove_credentials(self.config) self.strategylist: List[IStrategy] = [] + self.all_results: Dict[str, Dict] = {} + self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) dataprovider = DataProvider(self.config, self.exchange) @@ -424,6 +426,47 @@ class Backtesting: return DataFrame.from_records(trades, columns=BacktestResult._fields) + def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): + logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) + self._set_strategy(strat) + + # Use max_open_trades in backtesting, except --disable-max-market-positions is set + if self.config.get('use_max_market_positions', True): + # Must come from strategy config, as the strategy may modify this setting. + max_open_trades = self.strategy.config['max_open_trades'] + else: + logger.info( + 'Ignoring max_open_trades (--disable-max-market-positions was used) ...') + max_open_trades = 0 + + # need to reprocess data every time to populate signals + preprocessed = self.strategy.ohlcvdata_to_dataframe(data) + + # Trim startup period from analyzed dataframe + for pair, df in preprocessed.items(): + preprocessed[pair] = trim_dataframe(df, timerange) + min_date, max_date = history.get_timerange(preprocessed) + + logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'({(max_date - min_date).days} days)..') + # Execute backtest and store results + results = self.backtest( + processed=preprocessed, + stake_amount=self.config['stake_amount'], + start_date=min_date.datetime, + end_date=max_date.datetime, + max_open_trades=max_open_trades, + position_stacking=self.config.get('position_stacking', False), + enable_protections=self.config.get('enable_protections', False), + ) + self.all_results[self.strategy.get_strategy_name()] = { + 'results': results, + 'config': self.strategy.config, + 'locks': PairLocks.locks, + } + return min_date, max_date + def start(self) -> None: """ Run backtesting end-to-end @@ -431,55 +474,15 @@ class Backtesting: """ data: Dict[str, Any] = {} - logger.info('Using stake_currency: %s ...', self.config['stake_currency']) - logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - - position_stacking = self.config.get('position_stacking', False) - data, timerange = self.load_bt_data() - all_results = {} + min_date = None + max_date = None for strat in self.strategylist: - logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) - self._set_strategy(strat) + min_date, max_date = self.backtest_one_strategy(strat, data, timerange) - # Use max_open_trades in backtesting, except --disable-max-market-positions is set - if self.config.get('use_max_market_positions', True): - # Must come from strategy config, as the strategy may modify this setting. - max_open_trades = self.strategy.config['max_open_trades'] - else: - logger.info( - 'Ignoring max_open_trades (--disable-max-market-positions was used) ...') - max_open_trades = 0 - - # need to reprocess data every time to populate signals - preprocessed = self.strategy.ohlcvdata_to_dataframe(data) - - # Trim startup period from analyzed dataframe - for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange) - min_date, max_date = history.get_timerange(preprocessed) - - logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'({(max_date - min_date).days} days)..') - # Execute backtest and print results - results = self.backtest( - processed=preprocessed, - stake_amount=self.config['stake_amount'], - start_date=min_date.datetime, - end_date=max_date.datetime, - max_open_trades=max_open_trades, - position_stacking=position_stacking, - enable_protections=self.config.get('enable_protections', False), - ) - all_results[self.strategy.get_strategy_name()] = { - 'results': results, - 'config': self.strategy.config, - 'locks': PairLocks.locks, - } - - stats = generate_backtest_stats(data, all_results, min_date=min_date, max_date=max_date) + stats = generate_backtest_stats(data, self.all_results, + min_date=min_date, max_date=max_date) if self.config.get('export', False): store_backtest_stats(self.config['exportfilename'], stats) From baa1142afa75c577ee4bfd50f283a0e345253ebd Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Sep 2020 07:45:47 +0200 Subject: [PATCH 181/563] Use preprocessed to get min/max date in hyperopt --- freqtrade/optimize/backtesting.py | 4 ++++ freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fff3914a5..87eb8cb05 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -152,6 +152,10 @@ class Backtesting: self.strategy.order_types['stoploss_on_exchange'] = False def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]: + """ + Loads backtest data and returns the data combined with the timerange + as tuple. + """ timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 2a2f5b472..d4b9f4c3b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -650,7 +650,7 @@ class Hyperopt: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe(df, timerange) - min_date, max_date = get_timerange(data) + min_date, max_date = get_timerange(preprocessed) logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' From 914710625940d8ac95a886c6f43c2f001f69af76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Oct 2020 19:50:37 +0200 Subject: [PATCH 182/563] call bot_loop_start() in backtesting to allow setup-code to run --- docs/bot-basics.md | 5 +++-- freqtrade/optimize/backtesting.py | 3 +++ tests/optimize/test_backtesting.py | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 44f493456..86fb18645 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -49,8 +49,9 @@ This loop will be repeated again and again until the bot is stopped. [backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated. * Load historic data for configured pairlist. -* Calculate indicators (calls `populate_indicators()`). -* Calls `populate_buy_trend()` and `populate_sell_trend()` +* Calls `bot_loop_start()` once. +* Calculate indicators (calls `populate_indicators()` once per pair). +* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair) * Loops per candle simulating entry and exit points. * Generate backtest report output diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 87eb8cb05..cb021c9ff 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -26,6 +26,7 @@ from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType +from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper logger = logging.getLogger(__name__) @@ -434,6 +435,8 @@ class Backtesting: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) + strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() + # Use max_open_trades in backtesting, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): # Must come from strategy config, as the strategy may modify this setting. diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 1f8f7cfd8..e55e166d9 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -350,6 +350,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: default_conf['timerange'] = '-1510694220' backtesting = Backtesting(default_conf) + backtesting.strategy.bot_loop_start = MagicMock() backtesting.start() # check the logs, that will contain the backtest result exists = [ @@ -359,6 +360,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: for line in exists: assert log_has(line, caplog) assert backtesting.strategy.dp._pairlists is not None + assert backtesting.strategy.bot_loop_start.call_count == 1 def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: From 0b65fe6afe4b4d219dbc0d58277f1c2ad545ce2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Jan 2021 07:47:03 +0100 Subject: [PATCH 183/563] Capture backtest start / end time --- freqtrade/optimize/backtesting.py | 6 +++++- freqtrade/optimize/optimize_reports.py | 6 ++++++ tests/optimize/test_optimize_reports.py | 5 ++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cb021c9ff..106d0f200 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -6,7 +6,7 @@ This module contains the backtesting logic import logging from collections import defaultdict from copy import deepcopy -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, NamedTuple, Optional, Tuple from pandas import DataFrame @@ -433,6 +433,7 @@ class Backtesting: def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) + backtest_start_time = datetime.now(timezone.utc) self._set_strategy(strat) strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() @@ -467,10 +468,13 @@ class Backtesting: position_stacking=self.config.get('position_stacking', False), enable_protections=self.config.get('enable_protections', False), ) + backtest_end_time = datetime.now(timezone.utc) self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, 'locks': PairLocks.locks, + 'backtest_start_time': int(backtest_start_time.timestamp()), + 'backtest_end_time': int(backtest_end_time.timestamp()), } return min_date, max_date diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 6c70b7c84..a2bb6277e 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -282,6 +282,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'backtest_end_ts': max_date.int_timestamp * 1000, 'backtest_days': backtest_days, + 'backtest_run_start_ts': content['backtest_start_time'], + 'backtest_run_end_ts': content['backtest_end_time'], + 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, 'market_change': market_change, 'pairlist': list(btdata.keys()), @@ -290,6 +293,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'max_open_trades': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), 'timeframe': config['timeframe'], + 'timerange': config.get('timerange', ''), + 'enable_protections': config.get('enable_protections', False), + 'strategy_name': strategy, # Parameters relevant for backtesting 'stoploss': config['stoploss'], 'trailing_stop': config.get('trailing_stop', False), diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index e194e7de4..f184cb125 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -77,7 +77,10 @@ def test_generate_backtest_stats(default_conf, testdatadir): SellType.ROI, SellType.FORCE_SELL] }), 'config': default_conf, - 'locks': []} + 'locks': [], + 'backtest_start_time': Arrow.utcnow().int_timestamp, + 'backtest_end_time': Arrow.utcnow().int_timestamp, + } } timerange = TimeRange.parse_timerange('1510688220-1510700340') min_date = Arrow.fromtimestamp(1510688220) From ce5ba1bb6e8c5792ee3a0922e74bf57e7990bd60 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Fri, 15 Jan 2021 00:14:11 +0000 Subject: [PATCH 184/563] Adding Conda installation process --- docs/installation.md | 239 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 227 insertions(+), 12 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index a23399441..d5a564544 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -110,9 +110,9 @@ OS Specific steps are listed first, the [Common](#common) section below is neces This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. - - ``` bash + + ```bash sudo apt-get install python3-venv libatlas-base-dev cmake # Use pywheels.org to speed up installation sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf @@ -185,7 +185,7 @@ git checkout stable #### 4. Install python dependencies -``` bash +```bash python3 -m pip install --upgrade pip python3 -m pip install -e . ``` @@ -218,19 +218,234 @@ On Linux, as an optional post-installation task, you may wish to setup the bot t ------ -### Anaconda +## Conda (Miniconda or Anaconda) -Freqtrade can also be installed using Anaconda (or Miniconda). +Freqtrade can also be installed with Miniconda or Anaconda. +Conda is a package manager and virtual environment manager in one. +The purpose of installing Conda (Miniconda or Anaconda) is to automatically prepare and manage the extensive library dependencies of the Freqtrade program. -!!! Note - This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. See below. +You can start with any clean distribution of Debian-based Linux distributions, which includes Ubuntu Linux. +For other popular Linux distributions, see: https://distrowatch.com/ or https://www.ubuntupit.com/best-debian-based-linux-distributions/ -``` bash -conda env create -f environment.yml +Installation requirement: Your Linux is a Debian based distribution with bash terminal + +To get know more about Conda check : https://linuxnetmag.com/miniconda-vs-anaconda/ + +Shortly : Conda < Miniconda < Anaconda + +https://www.dunderdata.com/blog/anaconda-is-bloated-set-up-a-lean-robust-data-science-environment-with-miniconda-and-conda-forge + +The difference lies in the number of packages that need to be installed upfront: how heavy/light the installation is. Difference is like ~1:8. It is recommended to install Miniconda instead of Anaconda. + +#### 1. Instal Conda + +##### Download conda file: + +installation file of chosen distribution you can find here + +https://docs.conda.io/en/latest/miniconda.html + +##### Verify file integrity with SHA256 checksum: + +official SHA256 checksum can be found here + +https://docs.conda.io/en/latest/miniconda_hashes.html + +Navigate to the directory with your file, and run SHA256 checksum of your file, check with following command + +```bash +sha256sum ./Miniconda3-number-of-the-package ``` +both checksum suppose to match + +##### Install conda: + +While in directory with your file, install conda + +```bash +bash ./Miniconda3-number-of-the-package +``` + +Confirm with yes all questions + +After installation, it is mandatory to turn your terminal OFF and ON again. + +CTRL + D (close terminal), CTRL + ALT + T (open terminal) + +#### 2. Prepare Conda environment + + +enter the conda base environment: + +```bash +conda activate base +``` + +If you want, you can prevent the (base) conda environment from being activated automatically as well. + +```bash +conda config --set auto_activate_base false +``` + + +##### Change the channels with upgrades: + +Conda as a package manager can download new packages from "channels". The best developed Conda channel, is not the default channel and is called `conda-forge`. The code below switches to it. + +```bash +# adding forge +conda config --env --add channels conda-forge + +# make it strict +conda config --env --set channel_priority strict +``` + +You can check the status of conda with the following code. + +```bash +conda info +conda config --show channels +conda config --show channel_priority +``` + +#### 3. Freqtrade Conda Environment + +Now you have conda, but only base environment, + +```bash# download freqtrade +conda env list +``` + +It is time to setup environment of the Freqtrade itself: + + +Conda `create` command, installs all nested dependencies, for the selected libraries automatically + +general structure of installation command is: + +```bash +conda create -n [name of the environment] [python version] [packages] +``` + +so it can be + +```bash +conda create -n freqtrade-conda python=3.8 pandas numpy ta-lib git wheel virtualenv +``` + +or if you expect, to use later jupiter for [data-analysis](data-analysis.md), use + +```bash +conda create -n freqtrade-jupyter-conda python=3.8 pandas numpy ta-lib git wheel virtualenv jupyter +``` + +the same works for [docker](docker.md), for spyder and other useful programs. + +Further read on the topic: + +https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db972389cd1 + +https://metager.de/meta/meta.ger3?eingabe=ardsdatascience+guide+to+conda+environment + +!!! NOTE: +It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent package into the old environment, +takes less time than installing a large, heavy dependent package into the old environment. + +##### Enter/exit freqtrade-conda venv: + +to enter choosen conda environment + +```bash +conda activate freqtrade-conda + +``` + +to exit conda environment + +```bash +conda deactivate +``` + +#### 4. Freqtrade Instalation + +Within the newly created freqtrade-conda environment, download and install Freqtrade. To process --install command, may take a few minutes + +```bash +# download freqtrade +git clone https://github.com/freqtrade/freqtrade.git + +# enter downloaded directory 'freqtrade' +cd freqtrade + +# run setup +./setup.sh --install +``` + +Create virtual environment (yes, python3-venv environment, inside conda-environment), where Freqtrade program can run. + +Running python3-venv seems to be requirement of the Freqtrade program itself. + +```bash +# create venv environment, inside hidden directory /freqtrede/.env +python3 -m venv ./.env/ + +# run the python3-venv environment +source ./.env/bin/activate + +# install last required package +pip install -e . +``` + +##### pip install within conda, a reminder: + +The documentation of conda says that pip should NOT be used within conda, because internal problems can occur. +However, they are rare + +That is, why the conda-forge channel is preferred: (and if no library is available in conda, you have to use pip) + +* more libraries are available (less need for pip) +* conda-forge works better with pip +* the libraries are newer + + +#### 5. You are ready + +You are ready to run, create the user directory and configuration file, run the program dry to verify that everything is working, and run a backtest to double check. + +```bash +freqtrade create-userdir --userdir user_data +freqtrade new-config --config config.json +freqtrade download-data -t 5m 15m 1h --days 100 +freqtrade trade --strategy SampleStrategy +freqtrade backtesting -s SampleStrategy +``` + +important shortcuts + +```bash +# activate base environment +conda activate + +# activate freqtrade-conda environment +conda activate freqtrade-conda + +#deactivate any conda environments +conda deactivate + +# list installed conda environments +conda env list + +# activation/deactivate the venv +source ./.env/bin/activate +deactivate +``` + +Happy trading! + + ----- -## Troubleshooting +## Troubleshooting ### MacOS installation error @@ -239,7 +454,7 @@ Newer versions of MacOS may have installation failed with errors like `error: co This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. For MacOS 10.14, this can be accomplished with the below command. -``` bash +```bash open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg ``` @@ -252,7 +467,7 @@ The errors you'll see happen during installation and are related to the installa You can install the necessary libraries with the following command: -``` bash +```bash brew install hdf5 c-blosc ``` From f72d53351cd9c10530db8e5f613685d9009abbcf Mon Sep 17 00:00:00 2001 From: nas- Date: Fri, 15 Jan 2021 00:13:11 +0100 Subject: [PATCH 185/563] Added ability to keep invalid pairs while expanding expand_pairlist --- docs/configuration.md | 2 +- docs/includes/pairlists.md | 2 +- freqtrade/exchange/exchange.py | 2 +- freqtrade/plugins/pairlist/IPairList.py | 10 +-- freqtrade/plugins/pairlist/StaticPairList.py | 4 +- .../plugins/pairlist/pairlist_helpers.py | 37 ++++++++--- freqtrade/plugins/pairlistmanager.py | 33 +++++++--- tests/exchange/test_exchange.py | 63 +++++++++---------- tests/plugins/test_pairlist.py | 32 ++++++++++ 9 files changed, 128 insertions(+), 57 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 13c7eb47b..33d117c91 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -81,7 +81,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String -| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List +| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting.Supports regex pairs as */BTC. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List | `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 8919c4e3d..1ad38cc73 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -35,7 +35,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged #### Static Pair List -By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. +By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. Also the pairlist does support wildcards (in regex-style) - so `*/BTC` will include all pairs with BTC as a stake. It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 489f70528..436c8e4e9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -336,7 +336,7 @@ class Exchange: if not self.markets: logger.warning('Unable to validate pairs (assuming they are correct).') return - extended_pairs = expand_pairlist(pairs, list(self.markets)) + extended_pairs = expand_pairlist(pairs, list(self.markets), keep_invalid=True) invalid_pairs = [] for pair in extended_pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index f9809aeb1..95d776ae6 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -124,19 +124,21 @@ class IPairList(LoggingMixin, ABC): """ return self._pairlistmanager.verify_blacklist(pairlist, logmethod) - def verify_whitelist(self, pairlist: List[str], logmethod) -> List[str]: + def verify_whitelist(self, pairlist: List[str], logmethod, + keep_invalid: bool = False) -> List[str]: """ Proxy method to verify_whitelist for easy access for child classes. :param pairlist: Pairlist to validate - :param logmethod: Function that'll be called, `logger.info` or `logger.warning`. + :param logmethod: Function that'll be called, `logger.info` or `logger.warning` + :param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes. :return: pairlist - whitelisted pairs """ - return self._pairlistmanager.verify_whitelist(pairlist, logmethod) + return self._pairlistmanager.verify_whitelist(pairlist, logmethod, keep_invalid) def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]: """ Check available markets and remove pair from whitelist if necessary - :param whitelist: the sorted list of pairs the user might want to trade + :param pairlist: the sorted list of pairs the user might want to trade :return: the list of pairs the user wants to trade without those unavailable or black_listed """ diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index c216a322d..c5ced48c9 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -50,7 +50,9 @@ class StaticPairList(IPairList): :return: List of pairs """ if self._allow_inactive: - return self.verify_whitelist(self._config['exchange']['pair_whitelist'], logger.info) + return self.verify_whitelist( + self._config['exchange']['pair_whitelist'], logger.info, keep_invalid=True + ) else: return self._whitelist_for_active_markets( self.verify_whitelist(self._config['exchange']['pair_whitelist'], logger.info)) diff --git a/freqtrade/plugins/pairlist/pairlist_helpers.py b/freqtrade/plugins/pairlist/pairlist_helpers.py index 3352777f0..04320fe16 100644 --- a/freqtrade/plugins/pairlist/pairlist_helpers.py +++ b/freqtrade/plugins/pairlist/pairlist_helpers.py @@ -2,22 +2,41 @@ import re from typing import List -def expand_pairlist(wildcardpl: List[str], available_pairs: List[str]) -> List[str]: +def expand_pairlist(wildcardpl: List[str], available_pairs: List[str], + keep_invalid: bool = False) -> List[str]: """ Expand pairlist potentially containing wildcards based on available markets. This will implicitly filter all pairs in the wildcard-list which are not in available_pairs. :param wildcardpl: List of Pairlists, which may contain regex :param available_pairs: List of all available pairs (`exchange.get_markets().keys()`) + :param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes :return expanded pairlist, with Regexes from wildcardpl applied to match all available pairs. :raises: ValueError if a wildcard is invalid (like '*/BTC' - which should be `.*/BTC`) """ result = [] - for pair_wc in wildcardpl: - try: - comp = re.compile(pair_wc) - result += [ - pair for pair in available_pairs if re.match(comp, pair) - ] - except re.error as err: - raise ValueError(f"Wildcard error in {pair_wc}, {err}") + if keep_invalid: + for pair_wc in wildcardpl: + try: + comp = re.compile(pair_wc) + result_partial = [ + pair for pair in available_pairs if re.match(comp, pair) + ] + # Add all matching pairs. + # If there are no matching pairs (Pair not on exchange) keep it. + result += result_partial or [pair_wc] + except re.error as err: + raise ValueError(f"Wildcard error in {pair_wc}, {err}") + + for element in result: + if not re.fullmatch(r'^[A-Za-z0-9/-]+$', element): + result.remove(element) + else: + for pair_wc in wildcardpl: + try: + comp = re.compile(pair_wc) + result += [ + pair for pair in available_pairs if re.match(comp, pair) + ] + except re.error as err: + raise ValueError(f"Wildcard error in {pair_wc}, {err}") return result diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index c471d7c51..15e186e6c 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -59,9 +59,15 @@ class PairListManager(): """The expanded blacklist (including wildcard expansion)""" return expand_pairlist(self._blacklist, self._exchange.get_markets().keys()) + @property + def expanded_whitelist_keep_invalid(self) -> List[str]: + """The expanded whitelist (including wildcard expansion), maintaining invalid pairs""" + return expand_pairlist(self._whitelist, self._exchange.get_markets().keys(), + keep_invalid=True) + @property def expanded_whitelist(self) -> List[str]: - """The expanded whitelist (including wildcard expansion)""" + """The expanded whitelist (including wildcard expansion), filtering invalid pairs""" return expand_pairlist(self._whitelist, self._exchange.get_markets().keys()) @property @@ -134,19 +140,30 @@ class PairListManager(): pairlist.remove(pair) return pairlist - def verify_whitelist(self, pairlist: List[str], logmethod) -> List[str]: + def verify_whitelist(self, pairlist: List[str], logmethod, + keep_invalid: bool = False) -> List[str]: """ Verify and remove items from pairlist - returning a filtered pairlist. Logs a warning or info depending on `aswarning`. Pairlist Handlers explicitly using this method shall use `logmethod=logger.info` to avoid spamming with warning messages - :return: pairlist - blacklisted pairs + :param pairlist: Pairlist to validate + :param logmethod: Function that'll be called, `logger.info` or `logger.warning` + :param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes. + :return: pairlist - whitelisted pairs """ - try: - whitelist = self.expanded_whitelist - except ValueError as err: - logger.error(f"Pair blacklist contains an invalid Wildcard: {err}") - return [] + if keep_invalid: + try: + whitelist = self.expanded_whitelist_keep_invalid + except ValueError as err: + logger.error(f"Pair blacklist contains an invalid Wildcard: {err}") + return [] + else: + try: + whitelist = self.expanded_whitelist + except ValueError as err: + logger.error(f"Pair blacklist contains an invalid Wildcard: {err}") + return [] return whitelist def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8cd2a9bca..9d655997f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -505,38 +505,37 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d Exchange(default_conf) -# This cannot happen anymore as expand_pairlist implicitly filters out unavaliablie pairs -# def test_validate_pairs_not_available(default_conf, mocker): -# api_mock = MagicMock() -# type(api_mock).markets = PropertyMock(return_value={ -# 'XRP/BTC': {'inactive': True, 'base': 'XRP', 'quote': 'BTC'} -# }) -# mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) -# mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') -# mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') -# mocker.patch('freqtrade.exchange.Exchange._load_async_markets') -# -# with pytest.raises(OperationalException, match=r'not available'): -# Exchange(default_conf) -# -# -# def test_validate_pairs_exception(default_conf, mocker, caplog): -# caplog.set_level(logging.INFO) -# api_mock = MagicMock() -# mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) -# -# type(api_mock).markets = PropertyMock(return_value={}) -# mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) -# mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') -# mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') -# mocker.patch('freqtrade.exchange.Exchange._load_async_markets') -# -# with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): -# Exchange(default_conf) -# -# mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})) -# Exchange(default_conf) -# assert log_has('Unable to validate pairs (assuming they are correct).', caplog) +def test_validate_pairs_not_available(default_conf, mocker): + api_mock = MagicMock() + type(api_mock).markets = PropertyMock(return_value={ + 'XRP/BTC': {'inactive': True, 'base': 'XRP', 'quote': 'BTC'} + }) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') + + with pytest.raises(OperationalException, match=r'not available'): + Exchange(default_conf) + + +def test_validate_pairs_exception(default_conf, mocker, caplog): + caplog.set_level(logging.INFO) + api_mock = MagicMock() + mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) + + type(api_mock).markets = PropertyMock(return_value={}) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') + + with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): + Exchange(default_conf) + + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})) + Exchange(default_conf) + assert log_has('Unable to validate pairs (assuming they are correct).', caplog) def test_validate_pairs_restricted(default_conf, mocker, caplog): diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index d822f8319..e20e42c60 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -853,3 +853,35 @@ def test_expand_pairlist(wildcardlist, pairs, expected): expand_pairlist(wildcardlist, pairs) else: assert sorted(expand_pairlist(wildcardlist, pairs)) == sorted(expected) + + +@pytest.mark.parametrize('wildcardlist,pairs,expected', [ + (['BTC/USDT'], + ['BTC/USDT'], + ['BTC/USDT']), + (['BTC/USDT', 'ETH/USDT'], + ['BTC/USDT', 'ETH/USDT'], + ['BTC/USDT', 'ETH/USDT']), + (['BTC/USDT', 'ETH/USDT'], + ['BTC/USDT'], ['BTC/USDT', 'ETH/USDT']), # Test one too many + (['.*/USDT'], + ['BTC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETH/USDT']), # Wildcard simple + (['.*C/USDT'], + ['BTC/USDT', 'ETC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETC/USDT']), # Wildcard exclude one + (['.*UP/USDT', 'BTC/USDT', 'ETH/USDT'], + ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'], + ['BTC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT']), # Wildcard exclude one + (['BTC/.*', 'ETH/.*'], + ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP'], + ['BTC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP']), # Wildcard exclude one + (['*UP/USDT', 'BTC/USDT', 'ETH/USDT'], + ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'], + None), + (['HELLO/WORLD'], [], ['HELLO/WORLD']) # Invalid pair kept +]) +def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected): + if expected is None: + with pytest.raises(ValueError, match=r'Wildcard error in \*UP/USDT,'): + expand_pairlist(wildcardlist, pairs, keep_invalid=True) + else: + assert sorted(expand_pairlist(wildcardlist, pairs, keep_invalid=True)) == sorted(expected) From bf5868c96da6b0194c6bafa319f09bddef00d350 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Jan 2021 06:56:15 +0100 Subject: [PATCH 186/563] Add testcase for nonexisting pairs on whitelist --- docs/configuration.md | 2 +- docs/includes/pairlists.md | 2 +- freqtrade/plugins/pairlistmanager.py | 16 ++++++---------- tests/plugins/test_pairlist.py | 26 +++++++++++++++++++++++++- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 33d117c91..e655182b8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -81,7 +81,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String -| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting.Supports regex pairs as */BTC. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List +| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List | `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 1ad38cc73..2653406e7 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -35,7 +35,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged #### Static Pair List -By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. Also the pairlist does support wildcards (in regex-style) - so `*/BTC` will include all pairs with BTC as a stake. +By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. The pairlist also supports wildcards (in regex-style) - so `.*/BTC` will include all pairs with BTC as a stake. It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 15e186e6c..7ce77da59 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -152,18 +152,14 @@ class PairListManager(): :param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes. :return: pairlist - whitelisted pairs """ - if keep_invalid: - try: + try: + if keep_invalid: whitelist = self.expanded_whitelist_keep_invalid - except ValueError as err: - logger.error(f"Pair blacklist contains an invalid Wildcard: {err}") - return [] - else: - try: + else: whitelist = self.expanded_whitelist - except ValueError as err: - logger.error(f"Pair blacklist contains an invalid Wildcard: {err}") - return [] + except ValueError as err: + logger.error(f"Pair whitelist contains an invalid Wildcard: {err}") + return [] return whitelist def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes: diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index e20e42c60..910a9580c 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -156,6 +156,31 @@ def test_refresh_static_pairlist(mocker, markets, static_pl_conf): assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist +@pytest.mark.parametrize('pairs,expected', [ + (['NOEXIST/BTC', r'\+WHAT/BTC'], + ['ETH/BTC', 'TKN/BTC', 'TRST/BTC', 'NOEXIST/BTC', 'SWT/BTC', 'BCC/BTC', 'HOT/BTC']), + (['NOEXIST/BTC', r'*/BTC'], # This is an invalid regex + []), +]) +def test_refresh_static_pairlist_noexist(mocker, markets, static_pl_conf, pairs, expected, caplog): + + static_pl_conf['pairlists'][0]['allow_inactive'] = True + static_pl_conf['exchange']['pair_whitelist'] += pairs + freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + exchange_has=MagicMock(return_value=True), + markets=PropertyMock(return_value=markets), + ) + freqtrade.pairlists.refresh_pairlist() + + # Ensure all except those in whitelist are removed + assert set(expected) == set(freqtrade.pairlists.whitelist) + assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist + if not expected: + assert log_has_re(r'Pair whitelist contains an invalid Wildcard: Wildcard error.*', caplog) + + def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog): static_pl_conf['exchange']['pair_blacklist'] = ['*/BTC'] freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) @@ -165,7 +190,6 @@ def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog): markets=PropertyMock(return_value=markets), ) freqtrade.pairlists.refresh_pairlist() - # List ordered by BaseVolume whitelist = [] # Ensure all except those in whitelist are removed assert set(whitelist) == set(freqtrade.pairlists.whitelist) From d74376726a090b2489ddf61d45c10838d69b0fc1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Jan 2021 20:47:12 +0100 Subject: [PATCH 187/563] api-server should fully support max_open_trades=-1 --- freqtrade/rpc/rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7c9be89aa..1e304f01b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -121,7 +121,8 @@ class RPC: 'dry_run': config['dry_run'], 'stake_currency': config['stake_currency'], 'stake_amount': config['stake_amount'], - 'max_open_trades': config['max_open_trades'], + 'max_open_trades': (config['max_open_trades'] + if config['max_open_trades'] != float('inf') else -1), 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, 'stoploss': config.get('stoploss'), 'trailing_stop': config.get('trailing_stop'), From 9f338ba6ed79b65ac15e067fa316ae665a3ac139 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jan 2021 10:01:31 +0100 Subject: [PATCH 188/563] Debug random test failure in CI --- tests/rpc/test_rpc_apiserver.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5460519fe..518eb189e 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -523,13 +523,17 @@ def test_api_logs(botclient): assert isinstance(rc.json()['logs'][0][3], str) assert isinstance(rc.json()['logs'][0][4], str) - rc = client_get(client, f"{BASE_URI}/logs?limit=5") - assert_response(rc) - assert len(rc.json()) == 2 - assert 'logs' in rc.json() + rc1 = client_get(client, f"{BASE_URI}/logs?limit=5") + assert_response(rc1) + assert len(rc1.json()) == 2 + assert 'logs' in rc1.json() # Using a fixed comparison here would make this test fail! - assert rc.json()['log_count'] == 5 - assert len(rc.json()['logs']) == rc.json()['log_count'] + if rc1.json()['log_count'] == 0: + # Help debugging random test failure + print(f"{rc.json()=}") + print(f"{rc1.json()=}") + assert rc1.json()['log_count'] == 5 + assert len(rc1.json()['logs']) == rc1.json()['log_count'] def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): From 572f5f91861c0d2a5558b75b91c5853828803a38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jan 2021 10:05:47 +0100 Subject: [PATCH 189/563] Fix fstring syntax error --- tests/rpc/test_rpc_apiserver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 518eb189e..c782f5431 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -530,8 +530,8 @@ def test_api_logs(botclient): # Using a fixed comparison here would make this test fail! if rc1.json()['log_count'] == 0: # Help debugging random test failure - print(f"{rc.json()=}") - print(f"{rc1.json()=}") + print(f"rc={rc.json()}") + print(f"rc1={rc1.json()}") assert rc1.json()['log_count'] == 5 assert len(rc1.json()['logs']) == rc1.json()['log_count'] From 53c208197d12560f2e4b39338831ac509f3960bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jan 2021 16:19:49 +0100 Subject: [PATCH 190/563] Add bot_name setting allows naming the bot to simply differentiate when running different bots. --- config.json.example | 1 + config_binance.json.example | 1 + config_full.json.example | 1 + config_kraken.json.example | 1 + docs/configuration.md | 1 + freqtrade/constants.py | 1 + freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/rpc.py | 1 + freqtrade/templates/base_config.json.j2 | 1 + tests/rpc/test_rpc_apiserver.py | 1 + 10 files changed, 10 insertions(+) diff --git a/config.json.example b/config.json.example index fc59a4d5b..0f0bbec4b 100644 --- a/config.json.example +++ b/config.json.example @@ -85,6 +85,7 @@ "username": "freqtrader", "password": "SuperSecurePassword" }, + "bot_name": "freqtrade", "initial_state": "running", "forcebuy_enable": false, "internals": { diff --git a/config_binance.json.example b/config_binance.json.example index 954634def..83c9748d7 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -90,6 +90,7 @@ "username": "freqtrader", "password": "SuperSecurePassword" }, + "bot_name": "freqtrade", "initial_state": "running", "forcebuy_enable": false, "internals": { diff --git a/config_full.json.example b/config_full.json.example index ef791d267..6593750b4 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -177,6 +177,7 @@ "username": "freqtrader", "password": "SuperSecurePassword" }, + "bot_name": "freqtrade", "db_url": "sqlite:///tradesv3.sqlite", "initial_state": "running", "forcebuy_enable": false, diff --git a/config_kraken.json.example b/config_kraken.json.example index 4b33eb592..3cd90e5d3 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -95,6 +95,7 @@ "username": "freqtrader", "password": "SuperSecurePassword" }, + "bot_name": "freqtrade", "initial_state": "running", "forcebuy_enable": false, "internals": { diff --git a/docs/configuration.md b/docs/configuration.md index 6a05fc3a3..9a3126618 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -110,6 +110,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `api_server.verbosity` | Logging verbosity. `info` will print all RPC Calls, while "error" will only display errors.
**Datatype:** Enum, either `info` or `error`. Defaults to `info`. | `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String +| `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.
*Defaults to `freqtrade`*
**Datatype:** String | `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances.
**Datatype:** String, SQLAlchemy connect string | `initial_state` | Defines the initial application state. More information below.
*Defaults to `stopped`.*
**Datatype:** Enum, either `stopped` or `running` | `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below.
**Datatype:** Boolean diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d48ab635e..69301ca0e 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -116,6 +116,7 @@ CONF_SCHEMA = { 'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_only_offset_is_reached': {'type': 'boolean'}, + 'bot_name': {'type': 'string'}, 'unfilledtimeout': { 'type': 'object', 'properties': { diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 45f160008..c9e8aaceb 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -131,6 +131,7 @@ class ShowConfig(BaseModel): forcebuy_enabled: bool ask_strategy: Dict[str, Any] bid_strategy: Dict[str, Any] + bot_name: str state: str runmode: str diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1e304f01b..bee04ddb6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -129,6 +129,7 @@ class RPC: 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), + 'bot_name': config.get('bot_name', 'freqtrade'), 'timeframe': config.get('timeframe'), 'timeframe_ms': timeframe_to_msecs(config['timeframe'] ) if 'timeframe' in config else '', diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index b362690f9..f920843b2 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -63,6 +63,7 @@ "username": "", "password": "" }, + "bot_name": "freqtrade", "initial_state": "running", "forcebuy_enable": false, "internals": { diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c782f5431..935f43885 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -414,6 +414,7 @@ def test_api_show_config(botclient, mocker): assert rc.json()['timeframe_ms'] == 300000 assert rc.json()['timeframe_min'] == 5 assert rc.json()['state'] == 'running' + assert rc.json()['bot_name'] == 'freqtrade' assert not rc.json()['trailing_stop'] assert 'bid_strategy' in rc.json() assert 'ask_strategy' in rc.json() From a271c9e98ef68224ce5e75cd332fc7208fa12c47 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Sat, 16 Jan 2021 22:24:22 +0000 Subject: [PATCH 191/563] Update installation.md --- docs/installation.md | 93 ++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 60 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index d5a564544..5b56afcaf 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -220,61 +220,38 @@ On Linux, as an optional post-installation task, you may wish to setup the bot t ## Conda (Miniconda or Anaconda) -Freqtrade can also be installed with Miniconda or Anaconda. -Conda is a package manager and virtual environment manager in one. -The purpose of installing Conda (Miniconda or Anaconda) is to automatically prepare and manage the extensive library dependencies of the Freqtrade program. +Freqtrade can also be installed with Miniconda or Anaconda. Conda (Miniconda or Anaconda) would automatically prepare and manage the extensive library-dependencies of the Freqtrade program. -You can start with any clean distribution of Debian-based Linux distributions, which includes Ubuntu Linux. -For other popular Linux distributions, see: https://distrowatch.com/ or https://www.ubuntupit.com/best-debian-based-linux-distributions/ +##### What is Conda? -Installation requirement: Your Linux is a Debian based distribution with bash terminal +It is (1) Package, (2) dependency and (3) environment management for any language : https://docs.conda.io/projects/conda/en/latest/index.html -To get know more about Conda check : https://linuxnetmag.com/miniconda-vs-anaconda/ +Shortly : Conda < Miniconda < Anaconda. Check : https://linuxnetmag.com/miniconda-vs-anaconda/ -Shortly : Conda < Miniconda < Anaconda +It is recommended to install Miniconda, not Anaconda. The difference between both lies in the amount of packages that would be installed upfront. Difference is at least like 1:8. https://www.dunderdata.com/blog/anaconda-is-bloated-set-up-a-lean-robust-data-science-environment-with-miniconda-and-conda-forge -The difference lies in the number of packages that need to be installed upfront: how heavy/light the installation is. Difference is like ~1:8. It is recommended to install Miniconda instead of Anaconda. #### 1. Instal Conda -##### Download conda file: +3 step installation instruction can be found here : +https://conda.io/projects/conda/en/latest/user-guide/install/linux.html#install-linux-silent -installation file of chosen distribution you can find here +Confirm with `yes` all questions. After installation, it is mandatory to turn your terminal OFF and ON again. -https://docs.conda.io/en/latest/miniconda.html - -##### Verify file integrity with SHA256 checksum: - -official SHA256 checksum can be found here - -https://docs.conda.io/en/latest/miniconda_hashes.html - -Navigate to the directory with your file, and run SHA256 checksum of your file, check with following command - -```bash -sha256sum ./Miniconda3-number-of-the-package -``` - -both checksum suppose to match - -##### Install conda: - -While in directory with your file, install conda - -```bash -bash ./Miniconda3-number-of-the-package -``` - -Confirm with yes all questions - -After installation, it is mandatory to turn your terminal OFF and ON again. - -CTRL + D (close terminal), CTRL + ALT + T (open terminal) #### 2. Prepare Conda environment +##### Change the channels with upgrades: + +After opening terminal, you already will be in default `base` conda environment + +If you want, you can prevent the (base) conda environment from being activated automatically. + +```bash +conda config --set auto_activate_base false +``` enter the conda base environment: @@ -282,15 +259,6 @@ enter the conda base environment: conda activate base ``` -If you want, you can prevent the (base) conda environment from being activated automatically as well. - -```bash -conda config --set auto_activate_base false -``` - - -##### Change the channels with upgrades: - Conda as a package manager can download new packages from "channels". The best developed Conda channel, is not the default channel and is called `conda-forge`. The code below switches to it. ```bash @@ -311,16 +279,15 @@ conda config --show channel_priority #### 3. Freqtrade Conda Environment -Now you have conda, but only base environment, +Now you have conda, but only (base) environment, -```bash# download freqtrade +```bash conda env list ``` It is time to setup environment of the Freqtrade itself: - -Conda `create` command, installs all nested dependencies, for the selected libraries automatically +The conda command `create -n` automatically installs all nested dependencies for the selected libraries general structure of installation command is: @@ -340,7 +307,7 @@ or if you expect, to use later jupiter for [data-analysis](data-analysis.md), us conda create -n freqtrade-jupyter-conda python=3.8 pandas numpy ta-lib git wheel virtualenv jupyter ``` -the same works for [docker](docker.md), for spyder and other useful programs. +the same works: for [docker](docker.md), for spyder and other useful programs. Further read on the topic: @@ -348,9 +315,10 @@ https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db9 https://metager.de/meta/meta.ger3?eingabe=ardsdatascience+guide+to+conda+environment -!!! NOTE: -It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent package into the old environment, -takes less time than installing a large, heavy dependent package into the old environment. +!!! Note "New heavy packages" + It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent, GUI package, into previously set environment, + + Example: Spyder ##### Enter/exit freqtrade-conda venv: @@ -414,11 +382,16 @@ That is, why the conda-forge channel is preferred: (and if no library is availab You are ready to run, create the user directory and configuration file, run the program dry to verify that everything is working, and run a backtest to double check. ```bash +# Prerequisite freqtrade create-userdir --userdir user_data + +# set up config file +# make sure to run it set the `dry_run : true` as you start freqtrade new-config --config config.json -freqtrade download-data -t 5m 15m 1h --days 100 -freqtrade trade --strategy SampleStrategy -freqtrade backtesting -s SampleStrategy + + +# run `dry_run` trades +freqtrade trade --strategy SampleStrategy ``` important shortcuts From 9ad0817105f3406788fbe2cdd49d24bbad2c5ce1 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Sat, 16 Jan 2021 22:27:25 +0000 Subject: [PATCH 192/563] Update installation.md --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 5b56afcaf..25bbaa1dd 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -379,7 +379,7 @@ That is, why the conda-forge channel is preferred: (and if no library is availab #### 5. You are ready -You are ready to run, create the user directory and configuration file, run the program dry to verify that everything is working, and run a backtest to double check. +You are ready to run, create the user directory and configuration file [Bot Configuration](configuration.md), run the program `dry` to verify that everything is working, and run a backtest to double check. ```bash # Prerequisite From 63be27f6716ef53e68db66541e511060f1b7cd85 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Sat, 16 Jan 2021 22:28:23 +0000 Subject: [PATCH 193/563] Update installation.md --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 25bbaa1dd..90ea714c2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -379,7 +379,7 @@ That is, why the conda-forge channel is preferred: (and if no library is availab #### 5. You are ready -You are ready to run, create the user directory and configuration file [Bot Configuration](configuration.md), run the program `dry` to verify that everything is working, and run a backtest to double check. +You are ready to run, create the user directory and configuration file [Bot Configuration](configuration.md), run the program `dry_run: True` to verify that everything works, and run a backtest to double check. ```bash # Prerequisite From 73206a919447339f334aa8ff68bbd0a3e14eb37f Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Sat, 16 Jan 2021 22:35:42 +0000 Subject: [PATCH 194/563] Update installation.md --- docs/installation.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 90ea714c2..aa4cde12d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -224,7 +224,7 @@ Freqtrade can also be installed with Miniconda or Anaconda. Conda (Miniconda or ##### What is Conda? -It is (1) Package, (2) dependency and (3) environment management for any language : https://docs.conda.io/projects/conda/en/latest/index.html +It is: (1) package, (2) dependency and (3) environment management for any programming language : https://docs.conda.io/projects/conda/en/latest/index.html Shortly : Conda < Miniconda < Anaconda. Check : https://linuxnetmag.com/miniconda-vs-anaconda/ @@ -316,9 +316,8 @@ https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db9 https://metager.de/meta/meta.ger3?eingabe=ardsdatascience+guide+to+conda+environment !!! Note "New heavy packages" - It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent, GUI package, into previously set environment, + It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent, GUI package, into previously set environment. Great example: Spyder - Example: Spyder ##### Enter/exit freqtrade-conda venv: @@ -379,7 +378,7 @@ That is, why the conda-forge channel is preferred: (and if no library is availab #### 5. You are ready -You are ready to run, create the user directory and configuration file [Bot Configuration](configuration.md), run the program `dry_run: True` to verify that everything works, and run a backtest to double check. +You are ready to run, create the user directory and configuration file [Bot Configuration](configuration.md), run the program `dry_run: True` to verify that everything is working, and run a backtest to double check. ```bash # Prerequisite From f9dd74585eadae5948313bd5d5288a191c4ebea5 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Sat, 16 Jan 2021 22:39:11 +0000 Subject: [PATCH 195/563] Update installation.md --- docs/installation.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index aa4cde12d..65020831f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -328,15 +328,17 @@ conda activate freqtrade-conda ``` -to exit conda environment +to exit conda environment. ```bash conda deactivate ``` +Don't do it now however and stay in conda environment. + #### 4. Freqtrade Instalation -Within the newly created freqtrade-conda environment, download and install Freqtrade. To process --install command, may take a few minutes +When in `freqtrade-conda` environment, download and install freqtrade. Command `./setup.sh --install`, will take few minutes. ```bash # download freqtrade From 172a629c5853b97362ee879144a3e7f71e2bb790 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Sat, 16 Jan 2021 22:41:37 +0000 Subject: [PATCH 196/563] Update installation.md --- docs/installation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 65020831f..129ad1e36 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -369,12 +369,12 @@ pip install -e . ##### pip install within conda, a reminder: The documentation of conda says that pip should NOT be used within conda, because internal problems can occur. -However, they are rare +However, they are rare. -That is, why the conda-forge channel is preferred: (and if no library is available in conda, you have to use pip) +Nevertherless, that is why, the `conda-forge` channel is preferred: -* more libraries are available (less need for pip) -* conda-forge works better with pip +* more libraries are available (less need for `pip`) +* `conda-forge` works better with `pip` * the libraries are newer From 389db2fe7de254d3aa09dbf9c8035697469561a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Jan 2021 19:11:00 +0100 Subject: [PATCH 197/563] Enhance wording of docker quickstart --- docs/docker_quickstart.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index e25e1b050..85f5a4a2d 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -8,9 +8,7 @@ Start by downloading and installing Docker CE for your platform: * [Windows](https://docs.docker.com/docker-for-windows/install/) * [Linux](https://docs.docker.com/install/) -Optionally, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start). - -Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. +To simplify running freqtrade, please install [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start). ## Freqtrade with docker-compose @@ -83,7 +81,8 @@ The `SampleStrategy` is run by default. !!! Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. - Please always backtest the strategy and use dry-run for some time before risking real money! + Please always backtest your strategy and use dry-run for some time before risking real money! + You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md). Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above). @@ -93,16 +92,16 @@ docker-compose up -d #### Docker-compose logs -Logs will be located at: `user_data/logs/freqtrade.log`. -You can check the latest log with the command `docker-compose logs -f`. +Logs will be written to: `user_data/logs/freqtrade.log`. +You can also check the latest log with the command `docker-compose logs -f`. #### Database -The database will be at: `user_data/tradesv3.sqlite` +The database will be located at: `user_data/tradesv3.sqlite` #### Updating freqtrade with docker-compose -To update freqtrade when using `docker-compose` is as simple as running the following 2 commands: +Updating freqtrade when using `docker-compose` is as simple as running the following 2 commands: ``` bash # Download the latest image @@ -120,7 +119,7 @@ This will first pull the latest image, and will then restart the container with Advanced users may edit the docker-compose file further to include all possible options or arguments. -All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. +All freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. !!! Note "`docker-compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). From a8bae3a38161d3fc6396c0819bb24df45b505af1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Jan 2021 20:31:27 +0100 Subject: [PATCH 198/563] Don't update trade fees for dry-run orders --- freqtrade/freqtradebot.py | 4 ++++ tests/test_freqtradebot.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6dc8eacf9..926f22225 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -268,6 +268,10 @@ class FreqtradeBot(LoggingMixin): Update closed trades without close fees assigned. Only acts when Orders are in the database, otherwise the last orderid is unknown. """ + if self.config['dry_run']: + # Updating open orders in dry-run does not make sense and will fail. + return + trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees() for trade in trades: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e6aff3352..6257a7e0b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4368,6 +4368,19 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): freqtrade.update_closed_trades_without_assigned_fees() + # Does nothing for dry-run + trades = Trade.get_trades().all() + assert len(trades) == MOCK_TRADE_COUNT + for trade in trades: + assert trade.fee_open_cost is None + assert trade.fee_open_currency is None + assert trade.fee_close_cost is None + assert trade.fee_close_currency is None + + freqtrade.config['dry_run'] = False + + freqtrade.update_closed_trades_without_assigned_fees() + trades = Trade.get_trades().all() assert len(trades) == MOCK_TRADE_COUNT From 6d40814dbf3059fe8c23bede6a98d433ecd15651 Mon Sep 17 00:00:00 2001 From: Andreas Brunner Date: Sun, 17 Jan 2021 20:39:35 +0100 Subject: [PATCH 199/563] extend status bot command to query specific trades --- freqtrade/rpc/rpc.py | 10 +++++++--- freqtrade/rpc/telegram.py | 8 +++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1e304f01b..03108b0f4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -144,13 +144,17 @@ class RPC: } return val - def _rpc_trade_status(self) -> List[Dict[str, Any]]: + def _rpc_trade_status(self, trade_ids=None) -> List[Dict[str, Any]]: """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is a remotely exposed function """ - # Fetch open trade - trades = Trade.get_open_trades() + # Fetch open trades + if trade_ids: + trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)) + else: + trades = Trade.get_open_trades() + if not trades: raise RPCException('no active trade') else: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7ec67e5d0..d304188c2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -277,7 +277,13 @@ class Telegram(RPCHandler): return try: - results = self._rpc._rpc_trade_status() + + # Check if there's at least one numerical ID provided. If so, try to get only these trades. + trade_ids = None + if context.args and len(context.args) > 0: + trade_ids = [i for i in context.args if i.isnumeric()] + + results = self._rpc._rpc_trade_status(trade_ids=trade_ids) messages = [] for r in results: From 3ea33d1737ee82750d4d6b4d1707f6cc49d27cf1 Mon Sep 17 00:00:00 2001 From: Andreas Brunner Date: Sun, 17 Jan 2021 21:15:17 +0100 Subject: [PATCH 200/563] updating doc and help with new /status argument --- README.md | 2 +- docs/telegram-usage.md | 1 + freqtrade/rpc/telegram.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1031e4d67..c61116219 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor - `/start`: Starts the trader. - `/stop`: Stops the trader. - `/stopbuy`: Stop entering new trades. -- `/status [table]`: Lists all open trades. +- `/status |[table]`: Lists all or specific open trades. - `/profit`: Lists cumulative profit from all finished trades - `/forcesell |all`: Instantly sells the given trade (Ignoring `minimum_roi`). - `/performance`: Show performance of each finished trade grouped by pair diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 40481684d..57f2e98bd 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -137,6 +137,7 @@ official commands. You can ask at any moment for help with `/help`. | `/show_config` | Shows part of the current configuration with relevant settings to operation | `/logs [limit]` | Show last log messages. | `/status` | Lists all open trades +| `/status ` | Lists one or more specific trade. Separate multiple with a blank space. | `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) | `/trades [limit]` | List all recently closed trades in a table format. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d304188c2..abdaa948d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -821,7 +821,9 @@ class Telegram(RPCHandler): "Optionally takes a rate at which to buy.` \n") message = ("*/start:* `Starts the trader`\n" "*/stop:* `Stops the trader`\n" - "*/status [table]:* `Lists all open trades`\n" + "*/status |[table]:* `Lists all open trades`\n" + " * :* `Lists one or more specific trades.`\n" + " `Separate multiple with a blank space.`\n" " *table :* `will display trades in a table`\n" " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" From d21eff0d5231ca49c4a7a7544b6bc17e1137dbbb Mon Sep 17 00:00:00 2001 From: Andreas Brunner Date: Sun, 17 Jan 2021 21:21:31 +0100 Subject: [PATCH 201/563] fix, if an non existing trade_id is provided --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 03108b0f4..69e3d057d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -151,7 +151,7 @@ class RPC: """ # Fetch open trades if trade_ids: - trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)) + trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all() else: trades = Trade.get_open_trades() From eb95d970e9b029cb7b9778be1732f96e677cacfe Mon Sep 17 00:00:00 2001 From: Andreas Brunner Date: Sun, 17 Jan 2021 21:26:55 +0100 Subject: [PATCH 202/563] flake8 beautify --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index abdaa948d..c310f9803 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -278,7 +278,8 @@ class Telegram(RPCHandler): try: - # Check if there's at least one numerical ID provided. If so, try to get only these trades. + # Check if there's at least one numerical ID provided. + # If so, try to get only these trades. trade_ids = None if context.args and len(context.args) > 0: trade_ids = [i for i in context.args if i.isnumeric()] From 296a6bd43cb8b5f01303225fdb576b16d66c5cb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jan 2021 05:37:29 +0000 Subject: [PATCH 203/563] Bump coveralls from 2.2.0 to 3.0.0 Bumps [coveralls](https://github.com/coveralls-clients/coveralls-python) from 2.2.0 to 3.0.0. - [Release notes](https://github.com/coveralls-clients/coveralls-python/releases) - [Changelog](https://github.com/TheKevJames/coveralls-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/coveralls-clients/coveralls-python/compare/2.2.0...3.0.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 883c3089f..749450289 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==2.2.0 +coveralls==3.0.0 flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 From 6a8e495102fc0e79ab127727097eb3886cf19acc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jan 2021 05:37:37 +0000 Subject: [PATCH 204/563] Bump plotly from 4.14.1 to 4.14.3 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.14.1 to 4.14.3. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.14.1...v4.14.3) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 3e31a24ae..6693a593d 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.14.1 +plotly==4.14.3 From 7f8dbce367fe564acc13ace1cae07361c89f4139 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jan 2021 05:37:45 +0000 Subject: [PATCH 205/563] Bump mkdocs-material from 6.2.4 to 6.2.5 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.2.4 to 6.2.5. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.2.4...6.2.5) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index adf4bc96c..6fef05f0c 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.2.4 +mkdocs-material==6.2.5 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1 From 8b5f8937cc4d643b5f45f81d7b0f85cf76a4e7b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jan 2021 05:37:48 +0000 Subject: [PATCH 206/563] Bump pyjwt from 2.0.0 to 2.0.1 Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.0.0 to 2.0.1. - [Release notes](https://github.com/jpadilla/pyjwt/releases) - [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst) - [Commits](https://github.com/jpadilla/pyjwt/compare/2.0.0...2.0.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 74295c68e..5a44639a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ sdnotify==0.3.2 # API Server fastapi==0.63.0 uvicorn==0.13.3 -pyjwt==2.0.0 +pyjwt==2.0.1 # Support for colorized terminal output colorama==0.4.4 From 994b4013adcc83e8ad3a99c76a6974822e9bdcea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jan 2021 05:38:06 +0000 Subject: [PATCH 207/563] Bump ccxt from 1.40.30 to 1.40.74 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.40.30 to 1.40.74. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.40.30...1.40.74) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 74295c68e..3c91868a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.5 pandas==1.2.0 -ccxt==1.40.30 +ccxt==1.40.74 aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 From 10104927c914f44792933a4e513bf2886b72e1b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 18 Jan 2021 07:46:19 +0000 Subject: [PATCH 208/563] Fix devcontainer closes #4230 --- .devcontainer/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b333dc19d..19e09c969 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,13 +3,15 @@ FROM freqtradeorg/freqtrade:develop # Install dependencies COPY requirements-dev.txt /freqtrade/ RUN apt-get update \ - && apt-get -y install git sudo vim \ + && apt-get -y install git mercurial sudo vim \ && apt-get clean \ && pip install autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir \ && useradd -u 1000 -U -m ftuser \ && mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \ && echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \ && echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \ + && mv /root/.local /home/ftuser/.local/ \ + && chown ftuser:ftuser -R /home/ftuser/.local/ \ && chown ftuser: -R /home/ftuser/ USER ftuser From a68a546dd9fca6b1cf4abb67f4a4036280251bf8 Mon Sep 17 00:00:00 2001 From: Andreas Brunner Date: Mon, 18 Jan 2021 15:26:53 +0100 Subject: [PATCH 209/563] _rpc_trade_status argument datatype optimizations --- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/telegram.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 69e3d057d..f74d63408 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -144,7 +144,7 @@ class RPC: } return val - def _rpc_trade_status(self, trade_ids=None) -> List[Dict[str, Any]]: + def _rpc_trade_status(self, trade_ids: List[int] = []) -> List[Dict[str, Any]]: """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is a remotely exposed function diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c310f9803..99f9a8a91 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -280,9 +280,9 @@ class Telegram(RPCHandler): # Check if there's at least one numerical ID provided. # If so, try to get only these trades. - trade_ids = None + trade_ids = [] if context.args and len(context.args) > 0: - trade_ids = [i for i in context.args if i.isnumeric()] + trade_ids = [int(i) for i in context.args if i.isnumeric()] results = self._rpc._rpc_trade_status(trade_ids=trade_ids) From cd8d4da46696d64033e9d3a963d61948b2ee3cd3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Jan 2021 19:44:16 +0100 Subject: [PATCH 210/563] Add test for /status functionality --- tests/rpc/test_rpc_telegram.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a21a19e3a..1c34b6b26 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -205,13 +205,14 @@ def test_telegram_status(default_conf, update, mocker) -> None: assert msg_mock.call_count == 1 context = MagicMock() - # /status table 2 3 - context.args = ["table", "2", "3"] + # /status table + context.args = ["table"] telegram._status(update=update, context=context) assert status_table.call_count == 1 def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: + default_conf['max_open_trades'] = 3 mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -252,8 +253,23 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: assert 'Close Rate' not in ''.join(lines) assert 'Close Profit' not in ''.join(lines) - assert msg_mock.call_count == 1 + assert msg_mock.call_count == 3 assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] + assert 'LTC/BTC' in msg_mock.call_args_list[1][0][0] + + msg_mock.reset_mock() + context = MagicMock() + context.args = ["2", "3"] + + telegram._status(update=update, context=context) + + lines = msg_mock.call_args_list[0][0][0].split('\n') + assert '' not in lines + assert 'Close Rate' not in ''.join(lines) + assert 'Close Profit' not in ''.join(lines) + + assert msg_mock.call_count == 2 + assert 'LTC/BTC' in msg_mock.call_args_list[0][0][0] def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: From 7c99e6f0e6d8ffb4b2f256894a049a5e578aa259 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Jan 2021 20:49:28 +0100 Subject: [PATCH 211/563] Avoid random test failure --- tests/plugins/test_pairlocks.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index bd103b21e..dfcbff0ed 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -11,11 +11,10 @@ from freqtrade.persistence.models import PairLock @pytest.mark.usefixtures("init_persistence") def test_PairLocks(use_db): PairLocks.timeframe = '5m' + PairLocks.use_db = use_db # No lock should be present if use_db: assert len(PairLock.query.all()) == 0 - else: - PairLocks.use_db = False assert PairLocks.use_db == use_db @@ -88,10 +87,9 @@ def test_PairLocks(use_db): def test_PairLocks_getlongestlock(use_db): PairLocks.timeframe = '5m' # No lock should be present + PairLocks.use_db = use_db if use_db: assert len(PairLock.query.all()) == 0 - else: - PairLocks.use_db = False assert PairLocks.use_db == use_db From 86b3306a3b7910360a4abeb05eb1763a6f08f924 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Jan 2021 22:07:10 +0100 Subject: [PATCH 212/563] Small doc refactoring --- docs/configuration.md | 58 +++++++++++---------------------------- docs/strategy-advanced.md | 26 ++++++++++++++++++ 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9a3126618..781435271 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -277,6 +277,22 @@ before asking the strategy if we should buy or a sell an asset. After each wait every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or the static list of pairs) if we should buy. +### Ignoring expired candles + +When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it. + +In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the buy signal becomes expired. + +For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy: + +``` json + "ask_strategy":{ + "ignore_buying_expired_candle_after": 300, + "price_side": "bid", + // ... + }, +``` + ### Understand order_types The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. @@ -676,48 +692,6 @@ export HTTPS_PROXY="http://addr:port" freqtrade ``` -## Ignoring expired candles - -When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it. - -In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the buy signal becomes expired. - -For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy: - -``` jsonc - "ask_strategy":{ - "ignore_buying_expired_candle_after" = 300 # 5 minutes - "price_side": "bid", - // ... - }, -``` - -## Embedding Strategies - -Freqtrade provides you with with an easy way to embed the strategy into your configuration file. -This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field, -in your chosen config file. - -### Encoding a string as BASE64 - -This is a quick example, how to generate the BASE64 string in python - -```python -from base64 import urlsafe_b64encode - -with open(file, 'r') as f: - content = f.read() -content = urlsafe_b64encode(content.encode('utf-8')) -``` - -The variable 'content', will contain the strategy file in a BASE64 encoded form. Which can now be set in your configurations file as following - -```json -"strategy": "NameOfStrategy:BASE64String" -``` - -Please ensure that 'NameOfStrategy' is identical to the strategy name! - ## Next step Now you have configured your config.json, the next step is to [start your bot](bot-usage.md). diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 2431274d7..25d217d34 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -398,3 +398,29 @@ class MyAwesomeStrategy2(MyAwesomeStrategy): ``` Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need. + +## Embedding Strategies + +Freqtrade provides you with with an easy way to embed the strategy into your configuration file. +This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field, +in your chosen config file. + +### Encoding a string as BASE64 + +This is a quick example, how to generate the BASE64 string in python + +```python +from base64 import urlsafe_b64encode + +with open(file, 'r') as f: + content = f.read() +content = urlsafe_b64encode(content.encode('utf-8')) +``` + +The variable 'content', will contain the strategy file in a BASE64 encoded form. Which can now be set in your configurations file as following + +```json +"strategy": "NameOfStrategy:BASE64String" +``` + +Please ensure that 'NameOfStrategy' is identical to the strategy name! From 7c80eeea950a95138639c34f64f57d2e5fb14c18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Jan 2021 22:51:12 +0100 Subject: [PATCH 213/563] Add use_custom_stoploss to optimize_report --- freqtrade/optimize/optimize_reports.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index a2bb6277e..96ddb91a0 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -302,6 +302,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), + 'use_custom_stoploss': config.get('use_custom_stoploss', False), 'minimal_roi': config['minimal_roi'], 'use_sell_signal': config['ask_strategy']['use_sell_signal'], 'sell_profit_only': config['ask_strategy']['sell_profit_only'], From 992d6b801806fde03d8a4f011ebb91190def45ca Mon Sep 17 00:00:00 2001 From: Tijmen van den Brink Date: Wed, 20 Jan 2021 09:24:30 +0100 Subject: [PATCH 214/563] Small improvement to MaxDrawDown protection --- docs/includes/protections.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 95fa53ad2..3ae456c42 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -65,7 +65,7 @@ The below example stops trading for all pairs for 4 candles after the last trade `MaxDrawdown` uses all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after the last trade - assuming that the bot needs some time to let markets recover. -The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. +The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. ```json "protections": [ @@ -77,7 +77,6 @@ The below sample stops trading for 12 candles if max-drawdown is > 20% consideri "max_allowed_drawdown": 0.2 }, ], - ``` #### Low Profit Pairs From 5f5f75e147360a57dffe911c3a4bdb0de39680e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 20 Jan 2021 13:57:53 +0100 Subject: [PATCH 215/563] Improve wording in protections documentation --- docs/includes/protections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 3ae456c42..de34383ac 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -65,7 +65,7 @@ The below example stops trading for all pairs for 4 candles after the last trade `MaxDrawdown` uses all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after the last trade - assuming that the bot needs some time to let markets recover. -The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. +The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. ```json "protections": [ From 5c0f98b518274fc4f46bcee0449cffb17cc21d00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 20 Jan 2021 19:30:43 +0100 Subject: [PATCH 216/563] Blacklist Poloniex - as ccxt does not provide a fetch_order endpoint --- freqtrade/exchange/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index ce0fde9e4..c66db860f 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -21,6 +21,7 @@ BAD_EXCHANGES = { "hitbtc": "This API cannot be used with Freqtrade. " "Use `hitbtc2` exchange id to access this exchange.", "phemex": "Does not provide history. ", + "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.", **dict.fromkeys([ 'adara', 'anxpro', From fd379d36ac59a2d38e51486ad1a3a2dceab50458 Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 21 Jan 2021 12:27:22 +0100 Subject: [PATCH 217/563] Fixed quickstart link in docs --- docs/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docker.md b/docs/docker.md index f4699cf4c..1298296f1 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,7 +1,7 @@ ## Freqtrade with docker without docker-compose !!! Warning - The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker.md) instructions. + The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker_quickstart.md) instructions. ### Download the official Freqtrade docker image From c42241986e9f4d9b03e18b87c7cb87cbfd95f7db Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Jan 2021 19:20:38 +0100 Subject: [PATCH 218/563] further investigate random test failure --- tests/rpc/test_rpc_apiserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 935f43885..f5b9a58f3 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -529,7 +529,7 @@ def test_api_logs(botclient): assert len(rc1.json()) == 2 assert 'logs' in rc1.json() # Using a fixed comparison here would make this test fail! - if rc1.json()['log_count'] == 0: + if rc1.json()['log_count'] < 5: # Help debugging random test failure print(f"rc={rc.json()}") print(f"rc1={rc1.json()}") From e94e2dd383e787c254464cb5a43ca514895da269 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Jan 2021 17:32:57 +0100 Subject: [PATCH 219/563] Remove docker config without compose --- docs/docker.md | 201 ----------------------------------- docs/docker_quickstart.md | 7 +- docs/installation.md | 4 +- docs/windows_installation.md | 4 +- mkdocs.yml | 1 - 5 files changed, 10 insertions(+), 207 deletions(-) delete mode 100644 docs/docker.md diff --git a/docs/docker.md b/docs/docker.md deleted file mode 100644 index 1298296f1..000000000 --- a/docs/docker.md +++ /dev/null @@ -1,201 +0,0 @@ -## Freqtrade with docker without docker-compose - -!!! Warning - The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker_quickstart.md) instructions. - -### Download the official Freqtrade docker image - -Pull the image from docker hub. - -Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). - -```bash -docker pull freqtradeorg/freqtrade:stable -# Optionally tag the repository so the run-commands remain shorter -docker tag freqtradeorg/freqtrade:stable freqtrade -``` - -To update the image, simply run the above commands again and restart your running container. - -Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). - -!!! Note "Docker image update frequency" - The official docker images with tags `stable`, `develop` and `latest` are automatically rebuild once a week to keep the base image up-to-date. - In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. - -### Prepare the configuration files - -Even though you will use docker, you'll still need some files from the github repository. - -#### Clone the git repository - -Linux/Mac/Windows with WSL - -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -Windows with docker - -```bash -git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git -``` - -#### Copy `config.json.example` to `config.json` - -```bash -cd freqtrade -cp -n config.json.example config.json -``` - -> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. - -#### Create your database file - -=== "Dry-Run" - ``` bash - touch tradesv3.dryrun.sqlite - ``` - -=== "Production" - ``` bash - touch tradesv3.sqlite - ``` - - -!!! Warning "Database File Path" - Make sure to use the path to the correct database file when starting the bot in Docker. - -### Build your own Docker image - -Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. - -To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. - -```bash -docker build -t freqtrade -f docker/Dockerfile.technical . -``` - -If you are developing using Docker, use `docker/Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: - -```bash -docker build -f docker/Dockerfile.develop -t freqtrade-dev . -``` - -!!! Warning "Include your config file manually" - For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates. - -#### Verify the Docker image - -After the build process you can verify that the image was created with: - -```bash -docker images -``` - -The output should contain the freqtrade image. - -### Run the Docker image - -You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): - -```bash -docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -!!! Warning - In this example, the database will be created inside the docker instance and will be lost when you refresh your image. - -#### Adjust timezone - -By default, the container will use UTC timezone. -If you would like to change the timezone use the following commands: - -=== "Linux" - ``` bash - -v /etc/timezone:/etc/timezone:ro - - # Complete command: - docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade - ``` - -=== "MacOS" - ```bash - docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade - ``` - -!!! Note "MacOS Issues" - The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.
- A work-around for this is to start with the MacOS command above - More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). - -### Run a restartable docker image - -To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). - -#### 1. Move your config file and database - -The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. - -```bash -mkdir ~/.freqtrade -mv config.json ~/.freqtrade -mv tradesv3.sqlite ~/.freqtrade -``` - -#### 2. Run the docker image - -```bash -docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy -``` - -!!! Note - When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match. - -!!! Note - All available bot command line parameters can be added to the end of the `docker run` command. - -!!! Note - You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). - -### Monitor your Docker instance - -You can use the following commands to monitor and manage your container: - -```bash -docker logs freqtrade -docker logs -f freqtrade -docker restart freqtrade -docker stop freqtrade -docker start freqtrade -``` - -For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). - -!!! Note - You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. - -### Backtest with docker - -The following assumes that the download/setup of the docker image have been completed successfully. -Also, backtest-data should be available at `~/.freqtrade/user_data/`. - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ - freqtrade backtesting --strategy AwsomelyProfitableStrategy -``` - -Head over to the [Backtesting Documentation](backtesting.md) for more details. - -!!! Note - Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 85f5a4a2d..9cccfa93d 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -69,7 +69,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a !!! Question "How to edit the bot configuration?" You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. - You can also change the both Strategy and commands by editing the `docker-compose.yml` file. + You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. #### Adding a custom strategy @@ -90,6 +90,11 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li docker-compose up -d ``` +#### Monitoring the bot + +You can check for running instances with `docker-compose ps`. +This should list the service `freqtrade` as `running`. If that's not the case, best check the logs (see next point). + #### Docker-compose logs Logs will be written to: `user_data/logs/freqtrade.log`. diff --git a/docs/installation.md b/docs/installation.md index a23399441..8cb6724cb 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,7 +2,7 @@ This page explains how to prepare your environment for running the bot. -Please consider using the prebuilt [docker images](docker.md) to get started quickly while trying out freqtrade evaluating how it operates. +Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly while evaluating how freqtrade works. ## Prerequisite @@ -210,7 +210,7 @@ If this is the first time you run the bot, ensure you are running it in Dry-run freqtrade trade -c config.json ``` -*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. +*Note*: If you run the bot on a server, you should consider using [Docker compose](docker_quickstart.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. #### 7. (Optional) Post-installation Tasks diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 5341ce96b..168938973 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -1,4 +1,4 @@ -We **strongly** recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). +We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure). If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. Otherwise, try the instructions below. @@ -52,6 +52,6 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. +The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first. --- diff --git a/mkdocs.yml b/mkdocs.yml index 96cfa7651..4545e8d84 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,7 +3,6 @@ nav: - Home: index.md - Quickstart with Docker: docker_quickstart.md - Installation: - - Docker without docker-compose: docker.md - Linux/MacOS/Raspberry: installation.md - Windows: windows_installation.md - Freqtrade Basics: bot-basics.md From bec9b580b042a6baff158b651b0605632e2147d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Jan 2021 17:34:41 +0100 Subject: [PATCH 220/563] sell_profit_offset should be documented in the strategy override section --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 781435271..660dd6171 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -16,8 +16,7 @@ In some advanced use cases, multiple configuration files can be specified and us If you used the [Quick start](installation.md/#quick-start) method for installing the bot, the installation script should have already created the default configuration file (`config.json`) for you. -If default configuration file is not created we recommend you to copy and use the `config.json.example` as a template -for your bot configuration. +If default configuration file is not created we recommend you to use `freqtrade new-config --config config.json` to generate a basic configuration file. The Freqtrade configuration file is to be written in the JSON format. @@ -147,6 +146,7 @@ Values set in the configuration file always overwrite values set in the strategy * `protections` * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) +* `sell_profit_offset` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) * `ignore_buying_expired_candle_after` (ask_strategy) From 371b374ea610a059f80d1b20cc591cb72737ba1a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Jan 2021 19:12:34 +0100 Subject: [PATCH 221/563] Remove unused config setup from setup.sh --- setup.sh | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) diff --git a/setup.sh b/setup.sh index b270146c1..d0ca1f643 100755 --- a/setup.sh +++ b/setup.sh @@ -202,52 +202,6 @@ function test_and_fix_python_on_mac() { fi } -function config_generator() { - - echo "Starting to generate config.json" - echo - echo "Generating General configuration" - echo "-------------------------" - default_max_trades=3 - read -p "Max open trades: (Default: $default_max_trades) " max_trades - max_trades=${max_trades:-$default_max_trades} - - default_stake_amount=0.05 - read -p "Stake amount: (Default: $default_stake_amount) " stake_amount - stake_amount=${stake_amount:-$default_stake_amount} - - default_stake_currency="BTC" - read -p "Stake currency: (Default: $default_stake_currency) " stake_currency - stake_currency=${stake_currency:-$default_stake_currency} - - default_fiat_currency="USD" - read -p "Fiat currency: (Default: $default_fiat_currency) " fiat_currency - fiat_currency=${fiat_currency:-$default_fiat_currency} - - echo - echo "Generating exchange config " - echo "------------------------" - read -p "Exchange API key: " api_key - read -p "Exchange API Secret: " api_secret - - echo - echo "Generating Telegram config" - echo "-------------------------" - read -p "Telegram Token: " token - read -p "Telegram Chat_id: " chat_id - - sed -e "s/\"max_open_trades\": 3,/\"max_open_trades\": $max_trades,/g" \ - -e "s/\"stake_amount\": 0.05,/\"stake_amount\": $stake_amount,/g" \ - -e "s/\"stake_currency\": \"BTC\",/\"stake_currency\": \"$stake_currency\",/g" \ - -e "s/\"fiat_display_currency\": \"USD\",/\"fiat_display_currency\": \"$fiat_currency\",/g" \ - -e "s/\"your_exchange_key\"/\"$api_key\"/g" \ - -e "s/\"your_exchange_secret\"/\"$api_secret\"/g" \ - -e "s/\"your_telegram_token\"/\"$token\"/g" \ - -e "s/\"your_telegram_chat_id\"/\"$chat_id\"/g" \ - -e "s/\"dry_run\": false,/\"dry_run\": true,/g" config.json.example > config.json - -} - function config() { echo "-------------------------" From 31e0b09643796733a8d4918222347a90856c25b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Jan 2021 19:18:21 +0100 Subject: [PATCH 222/563] Rename config.json.example it's really the config dedicated to bittrex, so the name should reflect this in beeing config_bittrex.json.example --- .github/workflows/ci.yml | 12 ++--- .travis.yml | 4 +- MANIFEST.in | 1 - build_helpers/publish_docker.sh | 2 +- ...son.example => config_bittrex.json.example | 0 freqtrade/configuration/config_validation.py | 2 +- tests/commands/test_commands.py | 54 +++++++++---------- tests/test_arguments.py | 2 +- tests/test_main.py | 20 +++---- tests/test_plotting.py | 4 +- 10 files changed, 50 insertions(+), 51 deletions(-) rename config.json.example => config_bittrex.json.example (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 961dfef71..3f294347a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,13 +79,13 @@ jobs: - name: Backtesting run: | - cp config.json.example config.json + cp config_bittrex.json.example config.json freqtrade create-userdir --userdir user_data freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy - name: Hyperopt run: | - cp config.json.example config.json + cp config_bittrex.json.example config.json freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all @@ -171,13 +171,13 @@ jobs: - name: Backtesting run: | - cp config.json.example config.json + cp config_bittrex.json.example config.json freqtrade create-userdir --userdir user_data freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy - name: Hyperopt run: | - cp config.json.example config.json + cp config_bittrex.json.example config.json freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all @@ -238,13 +238,13 @@ jobs: - name: Backtesting run: | - cp config.json.example config.json + cp config_bittrex.json.example config.json freqtrade create-userdir --userdir user_data freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy - name: Hyperopt run: | - cp config.json.example config.json + cp config_bittrex.json.example config.json freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all diff --git a/.travis.yml b/.travis.yml index 94239e33f..03a8df49b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,12 +26,12 @@ jobs: # - coveralls || true name: pytest - script: - - cp config.json.example config.json + - cp config_bittrex.json.example config.json - freqtrade create-userdir --userdir user_data - freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy name: backtest - script: - - cp config.json.example config.json + - cp config_bittrex.json.example config.json - freqtrade create-userdir --userdir user_data - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily name: hyperopt diff --git a/MANIFEST.in b/MANIFEST.in index c67f5258f..2f59bcc7a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ include LICENSE include README.md -include config.json.example recursive-include freqtrade *.py recursive-include freqtrade/templates/ *.j2 *.ipynb diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index ac0cd2461..9bc1aa0a6 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -30,7 +30,7 @@ if [ $? -ne 0 ]; then fi # Run backtest -docker run --rm -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy +docker run --rm -v $(pwd)/config_bittrex.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy if [ $? -ne 0 ]; then echo "failed running backtest" diff --git a/config.json.example b/config_bittrex.json.example similarity index 100% rename from config.json.example rename to config_bittrex.json.example diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index b8829b80f..187b2e3c7 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -54,7 +54,7 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: return conf except ValidationError as e: logger.critical( - f"Invalid configuration. See config.json.example. Reason: {e}" + f"Invalid configuration. Reason: {e}" ) raise ValidationError( best_match(Draft4Validator(conf_schema).iter_errors(conf)).message diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 26e0c4a79..2284209a0 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -21,7 +21,7 @@ from tests.conftest_trades import MOCK_TRADE_COUNT def test_setup_utils_configuration(): args = [ - 'list-exchanges', '--config', 'config.json.example', + 'list-exchanges', '--config', 'config_bittrex.json.example', ] config = setup_utils_configuration(get_args(args), RunMode.OTHER) @@ -40,7 +40,7 @@ def test_start_trading_fail(mocker, caplog): exitmock = mocker.patch("freqtrade.worker.Worker.exit", MagicMock()) args = [ 'trade', - '-c', 'config.json.example' + '-c', 'config_bittrex.json.example' ] start_trading(get_args(args)) assert exitmock.call_count == 1 @@ -122,10 +122,10 @@ def test_list_timeframes(mocker, capsys): match=r"This command requires a configured exchange.*"): start_list_timeframes(pargs) - # Test with --config config.json.example + # Test with --config config_bittrex.json.example args = [ "list-timeframes", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', ] start_list_timeframes(get_args(args)) captured = capsys.readouterr() @@ -169,7 +169,7 @@ def test_list_timeframes(mocker, capsys): # Test with --one-column args = [ "list-timeframes", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--one-column", ] start_list_timeframes(get_args(args)) @@ -209,10 +209,10 @@ def test_list_markets(mocker, markets, capsys): match=r"This command requires a configured exchange.*"): start_list_markets(pargs, False) - # Test with --config config.json.example + # Test with --config config_bittrex.json.example args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--print-list", ] start_list_markets(get_args(args), False) @@ -239,7 +239,7 @@ def test_list_markets(mocker, markets, capsys): # Test with --all: all markets args = [ "list-markets", "--all", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--print-list", ] start_list_markets(get_args(args), False) @@ -252,7 +252,7 @@ def test_list_markets(mocker, markets, capsys): # Test list-pairs subcommand: active pairs args = [ "list-pairs", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--print-list", ] start_list_markets(get_args(args), True) @@ -264,7 +264,7 @@ def test_list_markets(mocker, markets, capsys): # Test list-pairs subcommand with --all: all pairs args = [ "list-pairs", "--all", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--print-list", ] start_list_markets(get_args(args), True) @@ -277,7 +277,7 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=ETH, LTC args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--base", "ETH", "LTC", "--print-list", ] @@ -290,7 +290,7 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=LTC args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--base", "LTC", "--print-list", ] @@ -303,7 +303,7 @@ def test_list_markets(mocker, markets, capsys): # active markets, quote=USDT, USD args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--quote", "USDT", "USD", "--print-list", ] @@ -316,7 +316,7 @@ def test_list_markets(mocker, markets, capsys): # active markets, quote=USDT args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--quote", "USDT", "--print-list", ] @@ -329,7 +329,7 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=LTC, quote=USDT args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--base", "LTC", "--quote", "USDT", "--print-list", ] @@ -342,7 +342,7 @@ def test_list_markets(mocker, markets, capsys): # active pairs, base=LTC, quote=USDT args = [ "list-pairs", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--base", "LTC", "--quote", "USD", "--print-list", ] @@ -355,7 +355,7 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=LTC, quote=USDT, NONEXISTENT args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--base", "LTC", "--quote", "USDT", "NONEXISTENT", "--print-list", ] @@ -368,7 +368,7 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=LTC, quote=NONEXISTENT args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--base", "LTC", "--quote", "NONEXISTENT", "--print-list", ] @@ -381,7 +381,7 @@ def test_list_markets(mocker, markets, capsys): # Test tabular output args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', ] start_list_markets(get_args(args), False) captured = capsys.readouterr() @@ -391,7 +391,7 @@ def test_list_markets(mocker, markets, capsys): # Test tabular output, no markets found args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--base", "LTC", "--quote", "NONEXISTENT", ] start_list_markets(get_args(args), False) @@ -403,7 +403,7 @@ def test_list_markets(mocker, markets, capsys): # Test --print-json args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--print-json" ] start_list_markets(get_args(args), False) @@ -415,7 +415,7 @@ def test_list_markets(mocker, markets, capsys): # Test --print-csv args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--print-csv" ] start_list_markets(get_args(args), False) @@ -427,7 +427,7 @@ def test_list_markets(mocker, markets, capsys): # Test --one-column args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--one-column" ] start_list_markets(get_args(args), False) @@ -439,7 +439,7 @@ def test_list_markets(mocker, markets, capsys): # Test --one-column args = [ "list-markets", - '--config', 'config.json.example', + '--config', 'config_bittrex.json.example', "--one-column" ] with pytest.raises(OperationalException, match=r"Cannot get markets.*"): @@ -781,7 +781,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): patched_configuration_load_config_file(mocker, default_conf) args = [ 'test-pairlist', - '-c', 'config.json.example' + '-c', 'config_bittrex.json.example' ] start_test_pairlist(get_args(args)) @@ -795,7 +795,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): args = [ 'test-pairlist', - '-c', 'config.json.example', + '-c', 'config_bittrex.json.example', '--one-column', ] start_test_pairlist(get_args(args)) @@ -804,7 +804,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): args = [ 'test-pairlist', - '-c', 'config.json.example', + '-c', 'config_bittrex.json.example', '--print-json', ] start_test_pairlist(get_args(args)) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index e2a1ae53c..60c2cfbac 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -172,7 +172,7 @@ def test_download_data_options() -> None: def test_plot_dataframe_options() -> None: args = [ 'plot-dataframe', - '-c', 'config.json.example', + '-c', 'config_bittrex.json.example', '--indicators1', 'sma10', 'sma100', '--indicators2', 'macd', 'fastd', 'fastk', '--plot-limit', '30', diff --git a/tests/test_main.py b/tests/test_main.py index f55aea336..70632aeaa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -67,12 +67,12 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) - args = ['trade', '-c', 'config.json.example'] + args = ['trade', '-c', 'config_bittrex.json.example'] # Test Main + the KeyboardInterrupt exception with pytest.raises(SystemExit): main(args) - assert log_has('Using config: config.json.example ...', caplog) + assert log_has('Using config: config_bittrex.json.example ...', caplog) assert log_has('Fatal exception!', caplog) @@ -85,12 +85,12 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) - args = ['trade', '-c', 'config.json.example'] + args = ['trade', '-c', 'config_bittrex.json.example'] # Test Main + the KeyboardInterrupt exception with pytest.raises(SystemExit): main(args) - assert log_has('Using config: config.json.example ...', caplog) + assert log_has('Using config: config_bittrex.json.example ...', caplog) assert log_has('SIGINT received, aborting ...', caplog) @@ -106,12 +106,12 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) - args = ['trade', '-c', 'config.json.example'] + args = ['trade', '-c', 'config_bittrex.json.example'] # Test Main + the KeyboardInterrupt exception with pytest.raises(SystemExit): main(args) - assert log_has('Using config: config.json.example ...', caplog) + assert log_has('Using config: config_bittrex.json.example ...', caplog) assert log_has('Oh snap!', caplog) @@ -157,12 +157,12 @@ def test_main_reload_config(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) - args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() + args = Arguments(['trade', '-c', 'config_bittrex.json.example']).get_parsed_arg() worker = Worker(args=args, config=default_conf) with pytest.raises(SystemExit): - main(['trade', '-c', 'config.json.example']) + main(['trade', '-c', 'config_bittrex.json.example']) - assert log_has('Using config: config.json.example ...', caplog) + assert log_has('Using config: config_bittrex.json.example ...', caplog) assert worker_mock.call_count == 4 assert reconfigure_mock.call_count == 1 assert isinstance(worker.freqtrade, FreqtradeBot) @@ -180,7 +180,7 @@ def test_reconfigure(mocker, default_conf) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) - args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() + args = Arguments(['trade', '-c', 'config_bittrex.json.example']).get_parsed_arg() worker = Worker(args=args, config=default_conf) freqtrade = worker.freqtrade diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 8f3ac464e..96c9868a9 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -363,7 +363,7 @@ def test_start_plot_dataframe(mocker): aup = mocker.patch("freqtrade.plot.plotting.load_and_plot_trades", MagicMock()) args = [ "plot-dataframe", - "--config", "config.json.example", + "--config", "config_bittrex.json.example", "--pairs", "ETH/BTC" ] start_plot_dataframe(get_args(args)) @@ -407,7 +407,7 @@ def test_start_plot_profit(mocker): aup = mocker.patch("freqtrade.plot.plotting.plot_profit", MagicMock()) args = [ "plot-profit", - "--config", "config.json.example", + "--config", "config_bittrex.json.example", "--pairs", "ETH/BTC" ] start_plot_profit(get_args(args)) From 16f96753564bbe791a7d4edc2b918a1ec58f92f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Jan 2021 20:35:10 +0100 Subject: [PATCH 223/563] Fix whitelist expansion problem --- freqtrade/plugins/pairlist/pairlist_helpers.py | 4 ++-- tests/plugins/test_pairlist.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/pairlist_helpers.py b/freqtrade/plugins/pairlist/pairlist_helpers.py index 04320fe16..924bfb293 100644 --- a/freqtrade/plugins/pairlist/pairlist_helpers.py +++ b/freqtrade/plugins/pairlist/pairlist_helpers.py @@ -19,7 +19,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str], try: comp = re.compile(pair_wc) result_partial = [ - pair for pair in available_pairs if re.match(comp, pair) + pair for pair in available_pairs if re.fullmatch(comp, pair) ] # Add all matching pairs. # If there are no matching pairs (Pair not on exchange) keep it. @@ -35,7 +35,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str], try: comp = re.compile(pair_wc) result += [ - pair for pair in available_pairs if re.match(comp, pair) + pair for pair in available_pairs if re.fullmatch(comp, pair) ] except re.error as err: raise ValueError(f"Wildcard error in {pair_wc}, {err}") diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 910a9580c..d62230e76 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -870,6 +870,9 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o (['*UP/USDT', 'BTC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'], None), + (['BTC/USD'], + ['BTC/USD', 'BTC/USDT'], + ['BTC/USD']), ]) def test_expand_pairlist(wildcardlist, pairs, expected): if expected is None: @@ -901,7 +904,11 @@ def test_expand_pairlist(wildcardlist, pairs, expected): (['*UP/USDT', 'BTC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'], None), - (['HELLO/WORLD'], [], ['HELLO/WORLD']) # Invalid pair kept + (['HELLO/WORLD'], [], ['HELLO/WORLD']), # Invalid pair kept + (['BTC/USD'], + ['BTC/USD', 'BTC/USDT'], + ['BTC/USD']), + ]) def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected): if expected is None: From 9a3c425cf408f2eacc7c16856bfe605ed7caaf0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Jan 2021 08:53:05 +0100 Subject: [PATCH 224/563] Update slack link --- CONTRIBUTING.md | 2 +- README.md | 2 +- docs/developer.md | 2 +- docs/faq.md | 2 +- docs/index.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c52a8e93..afa41ed33 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Few pointers for contributions: - New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. - PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). -If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. +If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index c61116219..db648198f 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ For any questions not covered by the documentation or for further information ab Please check out our [discord server](https://discord.gg/MA9v74M). -You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) diff --git a/docs/developer.md b/docs/developer.md index 299f2f77f..831d9d2f8 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,7 +2,7 @@ This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running. -All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) where you can ask questions. +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) where you can ask questions. ## Documentation diff --git a/docs/faq.md b/docs/faq.md index 5742f512a..8a0c61b29 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -143,7 +143,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: diff --git a/docs/index.md b/docs/index.md index 38e040d7a..b489861f0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,7 +65,7 @@ For any questions not covered by the documentation or for further information ab Please check out our [discord server](https://discord.gg/MA9v74M). -You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA). ## Ready to try? From 3b51545d234d5d83ef1aabacfd2e1f930827e9b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Jan 2021 12:43:27 +0100 Subject: [PATCH 225/563] Add trade_duration to to_json --- freqtrade/persistence/models.py | 3 +++ tests/rpc/test_rpc.py | 2 ++ tests/test_persistence.py | 2 ++ 3 files changed, 7 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e803b4383..492d2b941 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -302,6 +302,9 @@ class Trade(_DECL_BASE): 'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'close_profit_abs': self.close_profit_abs, # Deprecated + # TODO: should this be in minutes or seconds?? + 'trade_duration': (int((self.close_date - self.open_date).total_seconds() // 60) + if self.close_date else None), 'profit_ratio': self.close_profit, 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_abs': self.close_profit_abs, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 8ec356d54..69d79159a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -80,6 +80,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'amount': 91.07468123, 'amount_requested': 91.07468123, 'stake_amount': 0.001, + 'trade_duration': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, @@ -144,6 +145,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'current_rate': ANY, 'amount': 91.07468123, 'amount_requested': 91.07468123, + 'trade_duration': ANY, 'stake_amount': 0.001, 'close_profit': None, 'close_profit_pct': None, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 7487b2ef5..76c6ec9f6 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -815,6 +815,7 @@ def test_to_json(default_conf, fee): 'amount': 123.0, 'amount_requested': 123.0, 'stake_amount': 0.001, + 'trade_duration': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, @@ -869,6 +870,7 @@ def test_to_json(default_conf, fee): 'amount': 100.0, 'amount_requested': 101.0, 'stake_amount': 0.001, + 'trade_duration': 60, 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, From 48977493bb7bacaa37dfaf040be074ab080ec095 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Jan 2021 12:50:20 +0100 Subject: [PATCH 226/563] Backtesting does not need to convert to BacktestResult object --- freqtrade/optimize/backtesting.py | 57 +++++++++---------------- freqtrade/optimize/optimize_reports.py | 2 +- tests/optimize/test_backtesting.py | 6 +-- tests/optimize/test_optimize_reports.py | 2 +- 4 files changed, 25 insertions(+), 42 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 106d0f200..176ed34c5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -9,7 +9,7 @@ from copy import deepcopy from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, NamedTuple, Optional, Tuple -from pandas import DataFrame +from pandas import DataFrame, to_datetime from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency from freqtrade.constants import DATETIME_PRINT_FORMAT @@ -264,7 +264,7 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[BacktestResult]: + def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[Trade]: sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], @@ -276,25 +276,12 @@ class Backtesting: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = sell.sell_type trade.close(closerate, show_msg=False) + return trade - return BacktestResult(pair=trade.pair, - profit_percent=trade.calc_profit_ratio(rate=closerate), - profit_abs=trade.calc_profit(rate=closerate), - open_date=trade.open_date, - open_rate=trade.open_rate, - open_fee=self.fee, - close_date=sell_row[DATE_IDX], - close_rate=closerate, - close_fee=self.fee, - amount=trade.amount, - trade_duration=trade_dur, - open_at_end=False, - sell_reason=sell.sell_type - ) return None def handle_left_open(self, open_trades: Dict[str, List[Trade]], - data: Dict[str, List[Tuple]]) -> List[BacktestResult]: + data: Dict[str, List[Tuple]]) -> List[Trade]: """ Handling of left open trades at the end of backtesting """ @@ -304,24 +291,11 @@ class Backtesting: for trade in open_trades[pair]: sell_row = data[pair][-1] - trade_entry = BacktestResult(pair=trade.pair, - profit_percent=trade.calc_profit_ratio( - rate=sell_row[OPEN_IDX]), - profit_abs=trade.calc_profit(sell_row[OPEN_IDX]), - open_date=trade.open_date, - open_rate=trade.open_rate, - open_fee=self.fee, - close_date=sell_row[DATE_IDX], - close_rate=sell_row[OPEN_IDX], - close_fee=self.fee, - amount=trade.amount, - trade_duration=int(( - sell_row[DATE_IDX] - trade.open_date - ).total_seconds() // 60), - open_at_end=True, - sell_reason=SellType.FORCE_SELL - ) - trades.append(trade_entry) + trade.close_date = sell_row[DATE_IDX] + trade.sell_reason = SellType.FORCE_SELL + trade.close(sell_row[OPEN_IDX], show_msg=False) + trade.is_open = True + trades.append(trade) return trades def backtest(self, processed: Dict, stake_amount: float, @@ -348,7 +322,7 @@ class Backtesting: f"start_date: {start_date}, end_date: {end_date}, " f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" ) - trades = [] + trades: List[Trade] = [] self.prepare_backtest(enable_protections) # Use dict of lists with data for performance @@ -429,7 +403,16 @@ class Backtesting: trades += self.handle_left_open(open_trades, data=data) - return DataFrame.from_records(trades, columns=BacktestResult._fields) + cols = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', + 'open_fee', 'close_fee', 'trade_duration', + 'profit_ratio', 'profit_percent', 'profit_abs', 'sell_reason', + 'initial_stop_loss_abs', 'initial_stop_loss_ratio' 'stop_loss', 'stop_loss_ratio', + 'min_rate', 'max_rate', 'is_open', ] + df = DataFrame.from_records([t.to_json() for t in trades], columns=cols) + if len(df) > 0: + df.loc[:, 'close_date'] = to_datetime(df['close_date'], utc=True) + df.loc[:, 'open_date'] = to_datetime(df['open_date'], utc=True) + return df def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 96ddb91a0..1a4c2f269 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -253,7 +253,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 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']], + results=results.loc[results['is_open']], skip_nan=True) daily_stats = generate_daily_stats(results) best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index e55e166d9..fcac05a26 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -629,7 +629,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): # 100 buys signals assert len(results) == 100 # One trade was force-closed at the end - assert len(results.loc[results.open_at_end]) == 0 + assert len(results.loc[results['is_open']]) == 0 @pytest.mark.parametrize("pair", ['ADA/BTC', 'LTC/BTC']) @@ -811,7 +811,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'close_date': pd.to_datetime(['2018-01-29 20:45:00', '2018-01-30 05:35:00', ], utc=True), 'trade_duration': [235, 40], - 'open_at_end': [False, False], + 'is_open': [False, False], 'open_rate': [0.104445, 0.10302485], 'close_rate': [0.104969, 0.103541], 'sell_reason': [SellType.ROI, SellType.ROI] @@ -827,7 +827,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '2018-01-30 05:35:00', '2018-01-30 08:30:00'], utc=True), 'trade_duration': [47, 40, 20], - 'open_at_end': [False, False, False], + 'is_open': [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] diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index f184cb125..30dda1152 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -72,7 +72,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], + "is_open": [False, False, False, True], "sell_reason": [SellType.ROI, SellType.STOP_LOSS, SellType.ROI, SellType.FORCE_SELL] }), From 8ee264bc59cce3f44c6949b2b1ca1d93090e7e79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Jan 2021 13:02:48 +0100 Subject: [PATCH 227/563] Don't use profit_percent for backtesting results anymore --- freqtrade/optimize/backtesting.py | 29 ++-------------- freqtrade/optimize/default_hyperopt_loss.py | 2 +- freqtrade/optimize/hyperopt.py | 12 +++---- .../optimize/hyperopt_loss_onlyprofit.py | 2 +- freqtrade/optimize/hyperopt_loss_sharpe.py | 2 +- .../optimize/hyperopt_loss_sharpe_daily.py | 10 +++--- freqtrade/optimize/hyperopt_loss_sortino.py | 4 +-- .../optimize/hyperopt_loss_sortino_daily.py | 12 +++---- freqtrade/optimize/optimize_reports.py | 34 +++++++++---------- freqtrade/plot/plotting.py | 14 ++++---- freqtrade/templates/sample_hyperopt_loss.py | 4 +-- tests/optimize/conftest.py | 2 +- tests/optimize/test_backtest_detail.py | 2 +- tests/optimize/test_backtesting.py | 6 ++-- tests/optimize/test_hyperopt.py | 4 +-- tests/optimize/test_hyperoptloss.py | 24 ++++++------- tests/optimize/test_optimize_reports.py | 16 ++++----- 17 files changed, 78 insertions(+), 101 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 176ed34c5..08dbdffc4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -3,11 +3,12 @@ """ This module contains the backtesting logic """ +from freqtrade.data.btanalysis import BT_DATA_COLUMNS import logging from collections import defaultdict from copy import deepcopy from datetime import datetime, timedelta, timezone -from typing import Any, Dict, List, NamedTuple, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple from pandas import DataFrame, to_datetime @@ -41,25 +42,6 @@ LOW_IDX = 5 HIGH_IDX = 6 -class BacktestResult(NamedTuple): - """ - NamedTuple Defining BacktestResults inputs. - """ - pair: str - profit_percent: float - profit_abs: float - open_date: datetime - open_rate: float - open_fee: float - close_date: datetime - close_rate: float - close_fee: float - amount: float - trade_duration: float - open_at_end: bool - sell_reason: SellType - - class Backtesting: """ Backtesting class, this class contains all the logic to run a backtest @@ -403,12 +385,7 @@ class Backtesting: trades += self.handle_left_open(open_trades, data=data) - cols = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', - 'open_fee', 'close_fee', 'trade_duration', - 'profit_ratio', 'profit_percent', 'profit_abs', 'sell_reason', - 'initial_stop_loss_abs', 'initial_stop_loss_ratio' 'stop_loss', 'stop_loss_ratio', - 'min_rate', 'max_rate', 'is_open', ] - df = DataFrame.from_records([t.to_json() for t in trades], columns=cols) + df = DataFrame.from_records([t.to_json() for t in trades], columns=BT_DATA_COLUMNS) if len(df) > 0: df.loc[:, 'close_date'] = to_datetime(df['close_date'], utc=True) df.loc[:, 'open_date'] = to_datetime(df['open_date'], utc=True) diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 9dbdc4403..3712fd9a6 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -42,7 +42,7 @@ class ShortTradeDurHyperOptLoss(IHyperOptLoss): * 0.25: Avoiding trade loss * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above """ - total_profit = results['profit_percent'].sum() + total_profit = results['profit_ratio'].sum() trade_duration = results['trade_duration'].mean() trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d4b9f4c3b..20140492d 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -574,19 +574,19 @@ class Hyperopt: } def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: - wins = len(backtesting_results[backtesting_results.profit_percent > 0]) - draws = len(backtesting_results[backtesting_results.profit_percent == 0]) - losses = len(backtesting_results[backtesting_results.profit_percent < 0]) + wins = len(backtesting_results[backtesting_results['profit_ratio'] > 0]) + draws = len(backtesting_results[backtesting_results['profit_ratio'] == 0]) + losses = len(backtesting_results[backtesting_results['profit_ratio'] < 0]) return { 'trade_count': len(backtesting_results.index), 'wins': wins, 'draws': draws, 'losses': losses, 'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}", - 'avg_profit': backtesting_results.profit_percent.mean() * 100.0, - 'median_profit': backtesting_results.profit_percent.median() * 100.0, + 'avg_profit': backtesting_results['profit_ratio'].mean() * 100.0, + 'median_profit': backtesting_results['profit_ratio'].median() * 100.0, 'total_profit': backtesting_results.profit_abs.sum(), - 'profit': backtesting_results.profit_percent.sum() * 100.0, + 'profit': backtesting_results['profit_ratio'].sum() * 100.0, 'duration': backtesting_results.trade_duration.mean(), } diff --git a/freqtrade/optimize/hyperopt_loss_onlyprofit.py b/freqtrade/optimize/hyperopt_loss_onlyprofit.py index 43176dbad..33f3f5bc6 100644 --- a/freqtrade/optimize/hyperopt_loss_onlyprofit.py +++ b/freqtrade/optimize/hyperopt_loss_onlyprofit.py @@ -34,5 +34,5 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss): """ Objective function, returns smaller number for better results. """ - total_profit = results['profit_percent'].sum() + total_profit = results['profit_ratio'].sum() return 1 - total_profit / EXPECTED_MAX_PROFIT diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py index 232fb33b6..2c8ae552d 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -28,7 +28,7 @@ class SharpeHyperOptLoss(IHyperOptLoss): Uses Sharpe Ratio calculation. """ - total_profit = results["profit_percent"] + total_profit = results["profit_ratio"] days_period = (max_date - min_date).days # adding slippage of 0.1% per trade diff --git a/freqtrade/optimize/hyperopt_loss_sharpe_daily.py b/freqtrade/optimize/hyperopt_loss_sharpe_daily.py index bcba73a7f..9520123ee 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe_daily.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe_daily.py @@ -34,9 +34,9 @@ class SharpeHyperOptLossDaily(IHyperOptLoss): annual_risk_free_rate = 0.0 risk_free_rate = annual_risk_free_rate / days_in_year - # apply slippage per trade to profit_percent - results.loc[:, 'profit_percent_after_slippage'] = \ - results['profit_percent'] - slippage_per_trade_ratio + # apply slippage per trade to profit_ratio + results.loc[:, 'profit_ratio_after_slippage'] = \ + results['profit_ratio'] - slippage_per_trade_ratio # create the index within the min_date and end max_date t_index = date_range(start=min_date, end=max_date, freq=resample_freq, @@ -44,10 +44,10 @@ class SharpeHyperOptLossDaily(IHyperOptLoss): sum_daily = ( results.resample(resample_freq, on='close_date').agg( - {"profit_percent_after_slippage": sum}).reindex(t_index).fillna(0) + {"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0) ) - total_profit = sum_daily["profit_percent_after_slippage"] - risk_free_rate + total_profit = sum_daily["profit_ratio_after_slippage"] - risk_free_rate expected_returns_mean = total_profit.mean() up_stdev = total_profit.std() diff --git a/freqtrade/optimize/hyperopt_loss_sortino.py b/freqtrade/optimize/hyperopt_loss_sortino.py index c0ff0773a..b231370dd 100644 --- a/freqtrade/optimize/hyperopt_loss_sortino.py +++ b/freqtrade/optimize/hyperopt_loss_sortino.py @@ -28,7 +28,7 @@ class SortinoHyperOptLoss(IHyperOptLoss): Uses Sortino Ratio calculation. """ - total_profit = results["profit_percent"] + total_profit = results["profit_ratio"] days_period = (max_date - min_date).days # adding slippage of 0.1% per trade @@ -36,7 +36,7 @@ class SortinoHyperOptLoss(IHyperOptLoss): expected_returns_mean = total_profit.sum() / days_period results['downside_returns'] = 0 - results.loc[total_profit < 0, 'downside_returns'] = results['profit_percent'] + results.loc[total_profit < 0, 'downside_returns'] = results['profit_ratio'] down_stdev = np.std(results['downside_returns']) if down_stdev != 0: diff --git a/freqtrade/optimize/hyperopt_loss_sortino_daily.py b/freqtrade/optimize/hyperopt_loss_sortino_daily.py index 3b099a253..fac96664d 100644 --- a/freqtrade/optimize/hyperopt_loss_sortino_daily.py +++ b/freqtrade/optimize/hyperopt_loss_sortino_daily.py @@ -36,9 +36,9 @@ class SortinoHyperOptLossDaily(IHyperOptLoss): days_in_year = 365 minimum_acceptable_return = 0.0 - # apply slippage per trade to profit_percent - results.loc[:, 'profit_percent_after_slippage'] = \ - results['profit_percent'] - slippage_per_trade_ratio + # apply slippage per trade to profit_ratio + results.loc[:, 'profit_ratio_after_slippage'] = \ + results['profit_ratio'] - slippage_per_trade_ratio # create the index within the min_date and end max_date t_index = date_range(start=min_date, end=max_date, freq=resample_freq, @@ -46,17 +46,17 @@ class SortinoHyperOptLossDaily(IHyperOptLoss): sum_daily = ( results.resample(resample_freq, on='close_date').agg( - {"profit_percent_after_slippage": sum}).reindex(t_index).fillna(0) + {"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0) ) - total_profit = sum_daily["profit_percent_after_slippage"] - minimum_acceptable_return + total_profit = sum_daily["profit_ratio_after_slippage"] - minimum_acceptable_return expected_returns_mean = total_profit.mean() sum_daily['downside_returns'] = 0 sum_daily.loc[total_profit < 0, 'downside_returns'] = total_profit total_downside = sum_daily['downside_returns'] # Here total_downside contains min(0, P - MAR) values, - # where P = sum_daily["profit_percent_after_slippage"] + # where P = sum_daily["profit_ratio_after_slippage"] down_stdev = math.sqrt((total_downside**2).sum() / len(total_downside)) if down_stdev != 0: diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 1a4c2f269..4a9e833e7 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -58,14 +58,14 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: """ Generate one result dict, with "first_column" as key. """ - profit_sum = result['profit_percent'].sum() + profit_sum = result['profit_ratio'].sum() profit_total = profit_sum / max_open_trades return { 'key': first_column, 'trades': len(result), - 'profit_mean': result['profit_percent'].mean() if len(result) > 0 else 0.0, - 'profit_mean_pct': result['profit_percent'].mean() * 100.0 if len(result) > 0 else 0.0, + 'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0, + 'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0, 'profit_sum': profit_sum, 'profit_sum_pct': round(profit_sum * 100.0, 2), 'profit_total_abs': result['profit_abs'].sum(), @@ -124,8 +124,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List for reason, count in results['sell_reason'].value_counts().iteritems(): result = results.loc[results['sell_reason'] == reason] - profit_mean = result['profit_percent'].mean() - profit_sum = result['profit_percent'].sum() + profit_mean = result['profit_ratio'].mean() + profit_sum = result['profit_ratio'].sum() profit_total = profit_sum / max_open_trades tabular_data.append( @@ -150,7 +150,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List def generate_strategy_metrics(all_results: Dict) -> List[Dict]: """ Generate summary per strategy - :param all_results: Dict of containing results for all strategies + :param all_results: Dict of containing results for all strategies :return: List of Dicts containing the metrics per Strategy """ @@ -199,15 +199,15 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: 'winner_holding_avg': timedelta(), 'loser_holding_avg': timedelta(), } - daily_profit = results.resample('1d', on='close_date')['profit_percent'].sum() + daily_profit = results.resample('1d', on='close_date')['profit_ratio'].sum() worst = min(daily_profit) best = max(daily_profit) winning_days = sum(daily_profit > 0) draw_days = sum(daily_profit == 0) losing_days = sum(daily_profit < 0) - winning_trades = results.loc[results['profit_percent'] > 0] - losing_trades = results.loc[results['profit_percent'] < 0] + winning_trades = results.loc[results['profit_ratio'] > 0] + losing_trades = results.loc[results['profit_ratio'] < 0] return { 'backtest_best_day': best, @@ -273,8 +273,8 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), - 'profit_mean': results['profit_percent'].mean() if len(results) > 0 else 0, - 'profit_total': results['profit_percent'].sum(), + 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, + 'profit_total': results['profit_ratio'].sum(), 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, 'backtest_start_ts': min_date.int_timestamp * 1000, @@ -314,7 +314,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], try: max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( - results, value_col='profit_percent') + results, value_col='profit_ratio') strat_stats.update({ 'max_drawdown': max_drawdown, 'drawdown_start': drawdown_start, @@ -392,7 +392,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: Generate summary table 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 + :param all_results: Dict of containing results for all strategies :return: pretty printed table with tabulate as string """ floatfmt = _get_line_floatfmt() @@ -409,8 +409,8 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: - best_trade = max(strat_results['trades'], key=lambda x: x['profit_percent']) - worst_trade = min(strat_results['trades'], key=lambda x: x['profit_percent']) + best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio']) + worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio']) metrics = [ ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), @@ -424,9 +424,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), ('Worst Pair', f"{strat_results['worst_pair']['key']} " f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), - ('Best trade', f"{best_trade['pair']} {round(best_trade['profit_percent'] * 100, 2)}%"), + ('Best trade', f"{best_trade['pair']} {round(best_trade['profit_ratio'] * 100, 2)}%"), ('Worst trade', f"{worst_trade['pair']} " - f"{round(worst_trade['profit_percent'] * 100, 2)}%"), + f"{round(worst_trade['profit_ratio'] * 100, 2)}%"), ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 996c5276c..f45ba9b25 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -175,7 +175,7 @@ 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['profit_percent'] * 100, 1)}%, " + trades['desc'] = trades.apply(lambda row: f"{round(row['profit_ratio'] * 100, 1)}%, " f"{row['sell_reason']}, " f"{row['trade_duration']} min", axis=1) @@ -195,9 +195,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) trade_sells = go.Scatter( - x=trades.loc[trades['profit_percent'] > 0, "close_date"], - y=trades.loc[trades['profit_percent'] > 0, "close_rate"], - text=trades.loc[trades['profit_percent'] > 0, "desc"], + x=trades.loc[trades['profit_ratio'] > 0, "close_date"], + y=trades.loc[trades['profit_ratio'] > 0, "close_rate"], + text=trades.loc[trades['profit_ratio'] > 0, "desc"], mode='markers', name='Sell - Profit', marker=dict( @@ -208,9 +208,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) ) trade_sells_loss = go.Scatter( - x=trades.loc[trades['profit_percent'] <= 0, "close_date"], - y=trades.loc[trades['profit_percent'] <= 0, "close_rate"], - text=trades.loc[trades['profit_percent'] <= 0, "desc"], + x=trades.loc[trades['profit_ratio'] <= 0, "close_date"], + y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"], + text=trades.loc[trades['profit_ratio'] <= 0, "desc"], mode='markers', name='Sell - Loss', marker=dict( diff --git a/freqtrade/templates/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py index 59e6d814a..a2b28f948 100644 --- a/freqtrade/templates/sample_hyperopt_loss.py +++ b/freqtrade/templates/sample_hyperopt_loss.py @@ -39,8 +39,8 @@ class SampleHyperOptLoss(IHyperOptLoss): """ Objective function, returns smaller number for better results """ - total_profit = results.profit_percent.sum() - trade_duration = results.trade_duration.mean() + total_profit = results['profit_ratio'].sum() + trade_duration = results['trade_duration'].mean() trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index f06b0ecd3..df6f22e01 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -37,7 +37,7 @@ def hyperopt_results(): return pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [-0.1, 0.2, 0.3], + 'profit_ratio': [-0.1, 0.2, 0.3], 'profit_abs': [-0.2, 0.4, 0.6], 'trade_duration': [10, 30, 10], 'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI], diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 720ed8c13..daf7c2053 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -510,7 +510,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: ) assert len(results) == len(data.trades) - assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3) + assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3) for c, trade in enumerate(data.trades): res = results.iloc[c] diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index fcac05a26..54eeb7929 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -469,7 +469,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: expected = pd.DataFrame( {'pair': [pair, pair], - 'profit_percent': [0.0, 0.0], + 'profit_ratio': [0.0, 0.0], 'profit_abs': [0.0, 0.0], 'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True @@ -803,7 +803,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat patch_exchange(mocker) backtestmock = MagicMock(side_effect=[ pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], - 'profit_percent': [0.0, 0.0], + 'profit_ratio': [0.0, 0.0], 'profit_abs': [0.0, 0.0], 'open_date': pd.to_datetime(['2018-01-29 18:40:00', '2018-01-30 03:30:00', ], utc=True @@ -817,7 +817,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'sell_reason': [SellType.ROI, SellType.ROI] }), pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], - 'profit_percent': [0.03, 0.01, 0.1], + 'profit_ratio': [0.03, 0.01, 0.1], 'profit_abs': [0.01, 0.02, 0.2], 'open_date': pd.to_datetime(['2018-01-29 18:40:00', '2018-01-30 03:30:00', diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 82be894d3..89c7305b6 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -427,7 +427,7 @@ def test_format_results(hyperopt): ('LTC/BTC', 1, 1, 123), ('XPR/BTC', -1, -2, -246) ] - labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration'] + labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] df = pd.DataFrame.from_records(trades, columns=labels) results_metrics = hyperopt._calculate_results_metrics(df) results_explanation = hyperopt._format_results_explanation_string(results_metrics) @@ -567,7 +567,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: trades = [ ('TRX/BTC', 0.023117, 0.000233, 100) ] - labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration'] + labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] backtest_result = pd.DataFrame.from_records(trades, columns=labels) mocker.patch( diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index f7910e6d6..73feeb007 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -60,9 +60,9 @@ def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) correct = hl.hyperopt_loss_function(hyperopt_results, 600, @@ -77,9 +77,9 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) @@ -95,9 +95,9 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) @@ -113,9 +113,9 @@ def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) @@ -131,9 +131,9 @@ def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) @@ -149,9 +149,9 @@ def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_result def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 30dda1152..51a78c7cc 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -27,7 +27,7 @@ def test_text_table_bt_results(): results = pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2], + 'profit_ratio': [0.1, 0.2], 'profit_abs': [0.2, 0.4], 'trade_duration': [10, 30], 'wins': [2, 0], @@ -59,7 +59,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): results = {'DefStrat': { 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, Arrow(2017, 11, 14, 21, 36, 00).datetime, @@ -103,7 +103,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): results = {'DefStrat': { 'results': pd.DataFrame( {"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_percent": [0.003312, 0.010801, -0.013803, 0.002780], + "profit_ratio": [0.003312, 0.010801, -0.013803, 0.002780], "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003], "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, Arrow(2017, 11, 14, 21, 36, 00).datetime, @@ -179,7 +179,7 @@ def test_generate_pair_metrics(): results = pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2], + 'profit_ratio': [0.1, 0.2], 'profit_abs': [0.2, 0.4], 'trade_duration': [10, 30], 'wins': [2, 0], @@ -227,7 +227,7 @@ def test_text_table_sell_reason(): results = pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2, -0.1], + 'profit_ratio': [0.1, 0.2, -0.1], 'profit_abs': [0.2, 0.4, -0.2], 'trade_duration': [10, 30, 10], 'wins': [2, 0, 0], @@ -259,7 +259,7 @@ def test_generate_sell_reason_stats(): results = pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2, -0.1], + 'profit_ratio': [0.1, 0.2, -0.1], 'profit_abs': [0.2, 0.4, -0.2], 'trade_duration': [10, 30, 10], 'wins': [2, 0, 0], @@ -295,7 +295,7 @@ def test_text_table_strategy(default_conf): results['TestStrategy1'] = {'results': pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2, 0.3], + 'profit_ratio': [0.1, 0.2, 0.3], 'profit_abs': [0.2, 0.4, 0.5], 'trade_duration': [10, 30, 10], 'wins': [2, 0, 0], @@ -307,7 +307,7 @@ def test_text_table_strategy(default_conf): results['TestStrategy2'] = {'results': pd.DataFrame( { 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], - 'profit_percent': [0.4, 0.2, 0.3], + 'profit_ratio': [0.4, 0.2, 0.3], 'profit_abs': [0.4, 0.4, 0.5], 'trade_duration': [15, 30, 15], 'wins': [4, 1, 0], From deb8432d3395b6db8c03991e1321e3d144d2a580 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Jan 2021 20:49:49 +0100 Subject: [PATCH 228/563] Streamline trade to dataframe conversion --- freqtrade/data/btanalysis.py | 67 ++++++++++++++++--------------- freqtrade/optimize/backtesting.py | 8 +--- tests/data/test_btanalysis.py | 14 +++---- 3 files changed, 43 insertions(+), 46 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 2b51f5371..20977e447 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -2,9 +2,8 @@ Helpers when analyzing backtest data """ import logging -from datetime import timezone from pathlib import Path -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union import numpy as np import pandas as pd @@ -16,9 +15,21 @@ from freqtrade.persistence import Trade, init_db logger = logging.getLogger(__name__) -# must align with columns in backtest.py -BT_DATA_COLUMNS = ["pair", "profit_percent", "open_date", "close_date", "index", "trade_duration", - "open_rate", "close_rate", "open_at_end", "sell_reason"] +# Old format - maybe remove? +BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index", + "trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"] + +# Mid-term format, crated by BacktestResult Named Tuple +BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration', + 'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open', + 'fee_close', 'amount', 'profit_abs', 'profit_ratio'] + +# Newest format +BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', + 'fee_open', 'fee_close', 'trade_duration', + 'profit_ratio', 'profit_abs', 'sell_reason', + 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', + 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', ] def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: @@ -154,7 +165,7 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non ) else: # old format - only with lists. - df = pd.DataFrame(data, columns=BT_DATA_COLUMNS) + df = pd.DataFrame(data, columns=BT_DATA_COLUMNS_OLD) df['open_date'] = pd.to_datetime(df['open_date'], unit='s', @@ -166,7 +177,10 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non utc=True, infer_datetime_format=True ) + # Create compatibility with new format df['profit_abs'] = df['close_rate'] - df['open_rate'] + if 'profit_ratio' not in df.columns: + df['profit_ratio'] = df['profit_percent'] df = df.sort_values("open_date").reset_index(drop=True) return df @@ -209,6 +223,19 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str, return df_final[df_final['open_trades'] > max_open_trades] +def trade_list_to_dataframe(trades: List[Trade]) -> pd.DataFrame: + """ + Convert list of Trade objects to pandas Dataframe + :param trades: List of trade objects + :return: Dataframe with BT_DATA_COLUMNS + """ + df = pd.DataFrame.from_records([t.to_json() for t in trades], columns=BT_DATA_COLUMNS) + if len(df) > 0: + df.loc[:, 'close_date'] = pd.to_datetime(df['close_date'], utc=True) + df.loc[:, 'open_date'] = pd.to_datetime(df['open_date'], utc=True) + return df + + def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataFrame: """ Load trades from a DB (using dburl) @@ -219,36 +246,10 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF """ init_db(db_url, clean_open_orders=False) - columns = ["pair", "open_date", "close_date", "profit", "profit_percent", - "open_rate", "close_rate", "amount", "trade_duration", "sell_reason", - "fee_open", "fee_close", "open_rate_requested", "close_rate_requested", - "stake_amount", "max_rate", "min_rate", "id", "exchange", - "stop_loss", "initial_stop_loss", "strategy", "timeframe"] - filters = [] if strategy: filters.append(Trade.strategy == strategy) - - trades = pd.DataFrame([(t.pair, - t.open_date.replace(tzinfo=timezone.utc), - t.close_date.replace(tzinfo=timezone.utc) if t.close_date else None, - t.calc_profit(), t.calc_profit_ratio(), - t.open_rate, t.close_rate, t.amount, - (round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2) - if t.close_date else None), - t.sell_reason, - t.fee_open, t.fee_close, - t.open_rate_requested, - t.close_rate_requested, - t.stake_amount, - t.max_rate, - t.min_rate, - t.id, t.exchange, - t.stop_loss, t.initial_stop_loss, - t.strategy, t.timeframe - ) - for t in Trade.get_trades(filters).all()], - columns=columns) + trades = trade_list_to_dataframe(Trade.get_trades(filters).all()) return trades diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 08dbdffc4..875538731 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -3,7 +3,7 @@ """ This module contains the backtesting logic """ -from freqtrade.data.btanalysis import BT_DATA_COLUMNS +from freqtrade.data.btanalysis import BT_DATA_COLUMNS, trade_list_to_dataframe import logging from collections import defaultdict from copy import deepcopy @@ -385,11 +385,7 @@ class Backtesting: trades += self.handle_left_open(open_trades, data=data) - df = DataFrame.from_records([t.to_json() for t in trades], columns=BT_DATA_COLUMNS) - if len(df) > 0: - df.loc[:, 'close_date'] = to_datetime(df['close_date'], utc=True) - df.loc[:, 'open_date'] = to_datetime(df['open_date'], utc=True) - return df + return trade_list_to_dataframe(trades) def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index cdd5c08d2..9d6a31955 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -7,14 +7,14 @@ from pandas import DataFrame, DateOffset, Timestamp, to_datetime from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN -from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, - calculate_market_change, calculate_max_drawdown, +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_DATA_COLUMNS_OLD, + analyze_trade_parallelism, calculate_market_change, + calculate_max_drawdown, combine_dataframes_with_mean, create_cum_profit, extract_trades_of_period, get_latest_backtest_filename, get_latest_hyperopt_file, load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history -from freqtrade.optimize.backtesting import BacktestResult from tests.conftest import create_mock_trades from tests.conftest_trades import MOCK_TRADE_COUNT @@ -55,7 +55,7 @@ def test_load_backtest_data_old_format(testdatadir): filename = testdatadir / "backtest-result_test.json" bt_data = load_backtest_data(filename) assert isinstance(bt_data, DataFrame) - assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profit_abs"] + assert list(bt_data.columns) == BT_DATA_COLUMNS_OLD + ['profit_abs', 'profit_ratio'] assert len(bt_data) == 179 # Test loading from string (must yield same result) @@ -71,7 +71,7 @@ def test_load_backtest_data_new_format(testdatadir): filename = testdatadir / "backtest-result_new.json" bt_data = load_backtest_data(filename) assert isinstance(bt_data, DataFrame) - assert set(bt_data.columns) == set(list(BacktestResult._fields) + ["profit_abs"]) + assert set(bt_data.columns) == set(BT_DATA_COLUMNS_MID) assert len(bt_data) == 179 # Test loading from string (must yield same result) @@ -95,7 +95,7 @@ def test_load_backtest_data_multi(testdatadir): for strategy in ('DefaultStrategy', 'TestStrategy'): bt_data = load_backtest_data(filename, strategy=strategy) assert isinstance(bt_data, DataFrame) - assert set(bt_data.columns) == set(list(BacktestResult._fields) + ["profit_abs"]) + assert set(bt_data.columns) == set(BT_DATA_COLUMNS_MID) assert len(bt_data) == 179 # Test loading from string (must yield same result) @@ -122,7 +122,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_date" in trades.columns - assert "profit_percent" in trades.columns + assert "profit_ratio" in trades.columns for col in BT_DATA_COLUMNS: if col not in ['index', 'open_at_end']: From 9af89786ba450119a40b8e7da94ff11c8a3acbed Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Sun, 24 Jan 2021 17:03:56 +0000 Subject: [PATCH 229/563] update installation.md ma --- docs/installation.md | 129 +++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 85 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 129ad1e36..b1a730666 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -218,7 +218,7 @@ On Linux, as an optional post-installation task, you may wish to setup the bot t ------ -## Conda (Miniconda or Anaconda) +## Installation with Conda (Miniconda or Anaconda) Freqtrade can also be installed with Miniconda or Anaconda. Conda (Miniconda or Anaconda) would automatically prepare and manage the extensive library-dependencies of the Freqtrade program. @@ -228,9 +228,7 @@ It is: (1) package, (2) dependency and (3) environment management for any progra Shortly : Conda < Miniconda < Anaconda. Check : https://linuxnetmag.com/miniconda-vs-anaconda/ -It is recommended to install Miniconda, not Anaconda. The difference between both lies in the amount of packages that would be installed upfront. Difference is at least like 1:8. - -https://www.dunderdata.com/blog/anaconda-is-bloated-set-up-a-lean-robust-data-science-environment-with-miniconda-and-conda-forge +It is recommended to install Miniconda, not Anaconda. The difference between both lies in the amount of packages that would be installed upfront. Difference in weight is around like 1:8 : https://www.dunderdata.com/blog/anaconda-is-bloated-set-up-a-lean-robust-data-science-environment-with-miniconda-and-conda-forge #### 1. Instal Conda @@ -243,6 +241,25 @@ Confirm with `yes` all questions. After installation, it is mandatory to turn yo #### 2. Prepare Conda environment +##### Enter/exit freqtrade-conda venv: + +To check available environments, do + +```bash +conda env list +``` + +to enter or exit choosen conda environment + +```bash +# Enter environemt : `freqtrade-conda`, note that, this one, does not exist yet, and will be created in next steps +conda activate freqtrade-conda + +# Exit +conda deactivate + +``` + ##### Change the channels with upgrades: After opening terminal, you already will be in default `base` conda environment @@ -253,12 +270,6 @@ If you want, you can prevent the (base) conda environment from being activated a conda config --set auto_activate_base false ``` -enter the conda base environment: - -```bash -conda activate base -``` - Conda as a package manager can download new packages from "channels". The best developed Conda channel, is not the default channel and is called `conda-forge`. The code below switches to it. ```bash @@ -277,38 +288,41 @@ conda config --show channels conda config --show channel_priority ``` -#### 3. Freqtrade Conda Environment +#### 3. Freqtrade Instalation -Now you have conda, but only (base) environment, +Download and install freqtrade. ```bash -conda env list +# download freqtrade +git clone https://github.com/freqtrade/freqtrade.git + +# enter downloaded directory 'freqtrade' +cd freqtrade ``` -It is time to setup environment of the Freqtrade itself: +#### 4. Freqtrade Conda Environment + + +It is time to setup the working environment of the Freqtrade itself: The conda command `create -n` automatically installs all nested dependencies for the selected libraries general structure of installation command is: ```bash +# choose your own packages conda create -n [name of the environment] [python version] [packages] + +# point to packages in file +conda create -n [name of the environment] -f [file] ``` -so it can be +For installig freqtrade with conda, we would use file `environment.yml` which exist in freqtrade directory ```bash -conda create -n freqtrade-conda python=3.8 pandas numpy ta-lib git wheel virtualenv +conda create -n freqtrade-conda -f environment.yml ``` -or if you expect, to use later jupiter for [data-analysis](data-analysis.md), use - -```bash -conda create -n freqtrade-jupyter-conda python=3.8 pandas numpy ta-lib git wheel virtualenv jupyter -``` - -the same works: for [docker](docker.md), for spyder and other useful programs. - Further read on the topic: https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db972389cd1 @@ -319,59 +333,12 @@ https://metager.de/meta/meta.ger3?eingabe=ardsdatascience+guide+to+conda+environ It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent, GUI package, into previously set environment. Great example: Spyder -##### Enter/exit freqtrade-conda venv: - -to enter choosen conda environment - -```bash -conda activate freqtrade-conda - -``` - -to exit conda environment. - -```bash -conda deactivate -``` - -Don't do it now however and stay in conda environment. - -#### 4. Freqtrade Instalation - -When in `freqtrade-conda` environment, download and install freqtrade. Command `./setup.sh --install`, will take few minutes. - -```bash -# download freqtrade -git clone https://github.com/freqtrade/freqtrade.git - -# enter downloaded directory 'freqtrade' -cd freqtrade - -# run setup -./setup.sh --install -``` - -Create virtual environment (yes, python3-venv environment, inside conda-environment), where Freqtrade program can run. - -Running python3-venv seems to be requirement of the Freqtrade program itself. - -```bash -# create venv environment, inside hidden directory /freqtrede/.env -python3 -m venv ./.env/ - -# run the python3-venv environment -source ./.env/bin/activate - -# install last required package -pip install -e . -``` - ##### pip install within conda, a reminder: The documentation of conda says that pip should NOT be used within conda, because internal problems can occur. -However, they are rare. +However, they are rare. https://www.anaconda.com/blog/using-pip-in-a-conda-environment -Nevertherless, that is why, the `conda-forge` channel is preferred: +Nevertherless, that is why, the `conda-forge` channel is preferred: * more libraries are available (less need for `pip`) * `conda-forge` works better with `pip` @@ -389,15 +356,14 @@ freqtrade create-userdir --userdir user_data # set up config file # make sure to run it set the `dry_run : true` as you start freqtrade new-config --config config.json - - -# run `dry_run` trades -freqtrade trade --strategy SampleStrategy ``` important shortcuts ```bash +# list installed conda environments +conda env list + # activate base environment conda activate @@ -405,14 +371,7 @@ conda activate conda activate freqtrade-conda #deactivate any conda environments -conda deactivate - -# list installed conda environments -conda env list - -# activation/deactivate the venv -source ./.env/bin/activate -deactivate +conda deactivate ``` Happy trading! From 2c2a33b2e8918a5b2ebd44f11300768cefd5f334 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Sun, 24 Jan 2021 17:06:40 +0000 Subject: [PATCH 230/563] updated environemnt.ylm --- environment.yml | 113 ++++++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 51 deletions(-) diff --git a/environment.yml b/environment.yml index 746c4b912..38ca2d089 100644 --- a/environment.yml +++ b/environment.yml @@ -1,60 +1,71 @@ name: freqtrade channels: - - defaults + # - defaults - conda-forge dependencies: - # Required for app - - python>=3.7 - - pip - - wheel - - numpy - - pandas - - SQLAlchemy - - arrow - - requests - - urllib3 - - wrapt - - jsonschema - - tabulate - - python-rapidjson - - flask - - python-dotenv - - cachetools - - python-telegram-bot - # Optional for plotting - - plotly - # Optional for hyperopt - - scipy - - scikit-optimize - - scikit-learn - - filelock - - joblib - # Optional for development - - flake8 - - pytest - - pytest-mock - - pytest-asyncio - - pytest-cov - - coveralls - - mypy - # Useful for jupyter - - jupyter - - ipykernel - - isort - - yapf - - pip: - # Required for app - - cython - - pycoingecko - - ccxt +# 1/4 req main + - python>=3.7 + - numpy + - pandas + - pip + + - aiohttp + - SQLAlchemy + - python-telegram-bot + - arrow + - cachetools + - requests + - urllib3 + - wrapt + - jsonschema - TA-Lib - - py_find_1st + - tabulate + - jinja2 + - blosc - sdnotify - # Optional for develpment - - flake8-tidy-imports - - flake8-type-annotations - - pytest-random-order - - -e . + - fastapi + - uvicorn + - pyjwt + - colorama + - questionary + - prompt-toolkit + # ============================ + # 2/4 req dev + - coveralls + - flake8 + - mypy + - pytest + - pytest-asyncio + - pytest-cov + - pytest-mock= + - isort + - nbconvert + + # ============================ + # 3/4 req hyperopt + + - scipy + - scikit-learn + - filelock + - scikit-optimize + - joblib + - progressbar2 + # ============================ + # 4/4 req plot + + - plotly + - jupyter + + - pip: + - pycoingecko + - py_find_1st + - tables + - pytest-random-order + - flake8-type-annotations + - ccxt + - flake8-tidy-imports + - -e . + # - python-rapidjso From 789a980a3071eb547cb1de36924d4d55331e8f75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Jan 2021 09:56:27 +0100 Subject: [PATCH 231/563] Fix tests for new export format --- freqtrade/data/btanalysis.py | 2 ++ freqtrade/optimize/backtesting.py | 4 +-- tests/optimize/test_backtesting.py | 27 ++++++++++++------- .../testdata/backtest-result_multistrat.json | 2 +- tests/testdata/backtest-result_new.json | 2 +- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 20977e447..c004a4d23 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -26,6 +26,7 @@ BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'tra # Newest format BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', + 'open_rate', 'close_rate', 'fee_open', 'fee_close', 'trade_duration', 'profit_ratio', 'profit_abs', 'sell_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', @@ -233,6 +234,7 @@ def trade_list_to_dataframe(trades: List[Trade]) -> pd.DataFrame: if len(df) > 0: df.loc[:, 'close_date'] = pd.to_datetime(df['close_date'], utc=True) df.loc[:, 'open_date'] = pd.to_datetime(df['open_date'], utc=True) + df.loc[:, 'close_rate'] = df['close_rate'].astype('float64') return df diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 875538731..3186313e1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -3,18 +3,18 @@ """ This module contains the backtesting logic """ -from freqtrade.data.btanalysis import BT_DATA_COLUMNS, trade_list_to_dataframe import logging from collections import defaultdict from copy import deepcopy from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple -from pandas import DataFrame, to_datetime +from pandas import DataFrame from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data import history +from freqtrade.data.btanalysis import trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 54eeb7929..5f811e2e5 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -445,7 +445,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) -def test_backtest(default_conf, fee, mocker, testdatadir) -> None: +def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) @@ -469,21 +469,28 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: expected = pd.DataFrame( {'pair': [pair, pair], - 'profit_ratio': [0.0, 0.0], - 'profit_abs': [0.0, 0.0], + 'stake_amount': [0.001, 0.001], + 'amount': [0.00957442, 0.0097064], 'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True ), - 'open_rate': [0.104445, 0.10302485], - 'open_fee': [0.0025, 0.0025], 'close_date': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime, Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True), + 'open_rate': [0.104445, 0.10302485], 'close_rate': [0.104969, 0.103541], - 'close_fee': [0.0025, 0.0025], - 'amount': [0.00957442, 0.0097064], + 'fee_open': [0.0025, 0.0025], + 'fee_close': [0.0025, 0.0025], 'trade_duration': [235, 40], - 'open_at_end': [False, False], - 'sell_reason': [SellType.ROI, SellType.ROI] + 'profit_ratio': [0.0, 0.0], + 'profit_abs': [0.0, 0.0], + 'sell_reason': [SellType.ROI, SellType.ROI], + 'initial_stop_loss_abs': [0.0940005, 0.09272236], + 'initial_stop_loss_ratio': [-0.1, -0.1], + 'stop_loss_abs': [0.0940005, 0.09272236], + 'stop_loss_ratio': [-0.1, -0.1], + 'min_rate': [0.1038, 0.10302485], + 'max_rate': [0.10501, 0.1038888], + 'is_open': [False, False], }) pd.testing.assert_frame_equal(results, expected) data_pair = processed[pair] @@ -737,7 +744,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker) - backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS + ['profit_abs'])) + backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS)) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) diff --git a/tests/testdata/backtest-result_multistrat.json b/tests/testdata/backtest-result_multistrat.json index 0e5386ef3..6999050b6 100644 --- a/tests/testdata/backtest-result_multistrat.json +++ b/tests/testdata/backtest-result_multistrat.json @@ -1 +1 @@ -{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0, "pairlist": ["TRX/BTC", "ADA/BTC", "XLM/BTC", "ETH/BTC", "XMR/BTC", "ZEC/BTC","NXT/BTC", "LTC/BTC", "ETC/BTC", "DASH/BTC"]}, "TestStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0,"pairlist": ["TRX/BTC", "ADA/BTC", "XLM/BTC", "ETH/BTC", "XMR/BTC", "ZEC/BTC","NXT/BTC", "LTC/BTC", "ETC/BTC", "DASH/BTC"]}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}, {"key": "TestStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} +{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0, "pairlist": ["TRX/BTC", "ADA/BTC", "XLM/BTC", "ETH/BTC", "XMR/BTC", "ZEC/BTC","NXT/BTC", "LTC/BTC", "ETC/BTC", "DASH/BTC"]}, "TestStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0,"pairlist": ["TRX/BTC", "ADA/BTC", "XLM/BTC", "ETH/BTC", "XMR/BTC", "ZEC/BTC","NXT/BTC", "LTC/BTC", "ETC/BTC", "DASH/BTC"]}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}, {"key": "TestStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} diff --git a/tests/testdata/backtest-result_new.json b/tests/testdata/backtest-result_new.json index f004e879a..5334bf80e 100644 --- a/tests/testdata/backtest-result_new.json +++ b/tests/testdata/backtest-result_new.json @@ -1 +1 @@ -{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0, "pairlist": ["TRX/BTC", "ADA/BTC", "XLM/BTC", "ETH/BTC", "XMR/BTC", "ZEC/BTC","NXT/BTC", "LTC/BTC", "ETC/BTC", "DASH/BTC"]}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} +{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "fee_open": 0.0025, "fee_close": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0, "pairlist": ["TRX/BTC", "ADA/BTC", "XLM/BTC", "ETH/BTC", "XMR/BTC", "ZEC/BTC","NXT/BTC", "LTC/BTC", "ETC/BTC", "DASH/BTC"]}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} From 62e43539c95d6ce68763ffba0228ef0dabf2662f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Jan 2021 10:40:22 +0100 Subject: [PATCH 232/563] Limit max_open_trades to maximum available pairs closes #4008 --- docs/backtesting.md | 2 +- freqtrade/optimize/optimize_reports.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 27bfebe37..c3af0798b 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -264,7 +264,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - to clearly see settings for this. - `Total trades`: Identical to the total trades of the backtest output table. -- `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table. +- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 4a9e833e7..8edfbaf8d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -243,7 +243,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], if not isinstance(results, DataFrame): continue config = content['config'] - max_open_trades = config['max_open_trades'] + max_open_trades = min(config['max_open_trades'], len(btdata.keys())) stake_currency = config['stake_currency'] pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, @@ -274,7 +274,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, - 'profit_total': results['profit_ratio'].sum(), + 'profit_total': results['profit_ratio'].sum() / max_open_trades, 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, 'backtest_start_ts': min_date.int_timestamp * 1000, @@ -290,8 +290,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], - 'max_open_trades': (config['max_open_trades'] - if config['max_open_trades'] != float('inf') else -1), + 'max_open_trades': max_open_trades, + 'max_open_trades_setting': (config['max_open_trades'] + if config['max_open_trades'] != float('inf') else -1), 'timeframe': config['timeframe'], 'timerange': config.get('timerange', ''), 'enable_protections': config.get('enable_protections', False), From 2226f6781fd6070ba80e16add3df3689fbc96233 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Sun, 24 Jan 2021 21:31:36 +0000 Subject: [PATCH 233/563] Update installation.md --- docs/installation.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index b1a730666..c99f505db 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -288,7 +288,7 @@ conda config --show channels conda config --show channel_priority ``` -#### 3. Freqtrade Instalation +#### 3. Freqtrade Download Download and install freqtrade. @@ -300,7 +300,7 @@ git clone https://github.com/freqtrade/freqtrade.git cd freqtrade ``` -#### 4. Freqtrade Conda Environment +#### 4. Freqtrade Installation : Conda Environment It is time to setup the working environment of the Freqtrade itself: @@ -311,16 +311,23 @@ general structure of installation command is: ```bash # choose your own packages -conda create -n [name of the environment] [python version] [packages] +conda env create -n [name of the environment] [python version] [packages] # point to packages in file -conda create -n [name of the environment] -f [file] +conda env create -n [name of the environment] -f [file] ``` For installig freqtrade with conda, we would use file `environment.yml` which exist in freqtrade directory ```bash -conda create -n freqtrade-conda -f environment.yml +conda env create -n freqtrade-conda -f environment.yml +``` + +#### 4. Install python dependencies + +```bash +python3 -m pip install --upgrade pip +python3 -m pip install -e . ``` Further read on the topic: From c22cccb55b98a69b536b0b2b129f77a8da41dec7 Mon Sep 17 00:00:00 2001 From: "Tho Pham (Alex)" <20041501+thopd88@users.noreply.github.com> Date: Mon, 25 Jan 2021 12:24:47 +0700 Subject: [PATCH 234/563] Fix operator does not exist: boolean = integer --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 926f22225..db67e9771 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -200,7 +200,7 @@ class FreqtradeBot(LoggingMixin): Notify the user when the bot is stopped and there are still open trades active. """ - open_trades = Trade.get_trades([Trade.is_open == 1]).all() + open_trades = Trade.get_trades([Trade.is_open == True]).all() if len(open_trades) != 0: msg = { From b976baae3fe06267e64ae7809c15849bde6bdd03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 05:37:16 +0000 Subject: [PATCH 235/563] Bump cachetools from 4.2.0 to 4.2.1 Bumps [cachetools](https://github.com/tkem/cachetools) from 4.2.0 to 4.2.1. - [Release notes](https://github.com/tkem/cachetools/releases) - [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tkem/cachetools/compare/v4.2.0...v4.2.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 53e5bec12..268813e02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 arrow==0.17.0 -cachetools==4.2.0 +cachetools==4.2.1 requests==2.25.1 urllib3==1.26.2 wrapt==1.12.1 From 9422062cbd9456c95798a3b0ef49c27c037e5e3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 05:37:25 +0000 Subject: [PATCH 236/563] Bump blosc from 1.10.1 to 1.10.2 Bumps [blosc](https://github.com/blosc/python-blosc) from 1.10.1 to 1.10.2. - [Release notes](https://github.com/blosc/python-blosc/releases) - [Changelog](https://github.com/Blosc/python-blosc/blob/master/RELEASE_NOTES.rst) - [Commits](https://github.com/blosc/python-blosc/compare/v1.10.1...v1.10.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 53e5bec12..2fe4c63d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ tabulate==0.8.7 pycoingecko==1.4.0 jinja2==2.11.2 tables==3.6.1 -blosc==1.10.1 +blosc==1.10.2 # find first, C search in arrays py_find_1st==1.1.4 From afdcd2c0afb4ab3745cb853a1219b3393b6a2c11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 05:37:42 +0000 Subject: [PATCH 237/563] Bump pytest-cov from 2.10.1 to 2.11.1 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.10.1 to 2.11.1. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.10.1...v2.11.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 749450289..01066959a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ flake8-tidy-imports==4.2.1 mypy==0.790 pytest==6.2.1 pytest-asyncio==0.14.0 -pytest-cov==2.10.1 +pytest-cov==2.11.1 pytest-mock==3.5.1 pytest-random-order==1.0.4 isort==5.7.0 From d4e9037e6e15e3f380522bf7dc3ec3d85e967002 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 05:37:49 +0000 Subject: [PATCH 238/563] Bump pandas from 1.2.0 to 1.2.1 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.2.0 to 1.2.1. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.2.0...v1.2.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 53e5bec12..e19fe683d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.19.5 -pandas==1.2.0 +pandas==1.2.1 ccxt==1.40.74 aiohttp==3.7.3 From fb99cf14599c5a047e70703a493912ccfdfe41a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 05:37:50 +0000 Subject: [PATCH 239/563] Bump prompt-toolkit from 3.0.10 to 3.0.14 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.10 to 3.0.14. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.10...3.0.14) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 53e5bec12..cf2dcb0c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,4 +36,4 @@ pyjwt==2.0.1 colorama==0.4.4 # Building config files interactively questionary==1.9.0 -prompt-toolkit==3.0.10 +prompt-toolkit==3.0.14 From cb749b578dc5bd0a6879b95d445e9f071c4064c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 05:37:51 +0000 Subject: [PATCH 240/563] Bump scikit-learn from 0.24.0 to 0.24.1 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 0.24.0 to 0.24.1. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/0.24.0...0.24.1) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index fbb963cf9..104fbf454 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.6.0 -scikit-learn==0.24.0 +scikit-learn==0.24.1 scikit-optimize==0.8.1 filelock==3.0.12 joblib==1.0.0 From f98bd40955190fc388114ab4cc9062a7ef22ec89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 08:24:17 +0000 Subject: [PATCH 241/563] Bump ccxt from 1.40.74 to 1.40.99 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.40.74 to 1.40.99. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.40.74...1.40.99) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 471650685..cc8861d82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.5 pandas==1.2.1 -ccxt==1.40.74 +ccxt==1.40.99 aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 From c659150d9f0c51b5826e21aebd4ca8c01009b0eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Jan 2021 20:09:18 +0100 Subject: [PATCH 242/563] Also print trade_duration in seconds to json --- docs/backtesting.md | 2 +- freqtrade/optimize/hyperopt.py | 4 ++-- freqtrade/persistence/models.py | 4 +++- tests/rpc/test_rpc.py | 2 ++ tests/test_persistence.py | 2 ++ 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index c3af0798b..e1ab1c72d 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -262,7 +262,7 @@ It contains some useful key metrics about performance of your strategy on backte ``` - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). -- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - to clearly see settings for this. +- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). - `Total trades`: Identical to the total trades of the backtest output table. - `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 20140492d..d0cdceaeb 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -585,9 +585,9 @@ class Hyperopt: 'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}", 'avg_profit': backtesting_results['profit_ratio'].mean() * 100.0, 'median_profit': backtesting_results['profit_ratio'].median() * 100.0, - 'total_profit': backtesting_results.profit_abs.sum(), + 'total_profit': backtesting_results['profit_abs'].sum(), 'profit': backtesting_results['profit_ratio'].sum() * 100.0, - 'duration': backtesting_results.trade_duration.mean(), + 'duration': backtesting_results['trade_duration'].mean(), } def _format_results_explanation_string(self, results_metrics: Dict) -> str: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 492d2b941..375709423 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -302,9 +302,11 @@ class Trade(_DECL_BASE): 'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'close_profit_abs': self.close_profit_abs, # Deprecated - # TODO: should this be in minutes or seconds?? + 'trade_duration_s': (int((self.close_date - self.open_date).total_seconds()) + if self.close_date else None), 'trade_duration': (int((self.close_date - self.open_date).total_seconds() // 60) if self.close_date else None), + 'profit_ratio': self.close_profit, 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_abs': self.close_profit_abs, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 69d79159a..60d9950aa 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -81,6 +81,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'amount_requested': 91.07468123, 'stake_amount': 0.001, 'trade_duration': None, + 'trade_duration_s': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, @@ -146,6 +147,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'amount': 91.07468123, 'amount_requested': 91.07468123, 'trade_duration': ANY, + 'trade_duration_s': ANY, 'stake_amount': 0.001, 'close_profit': None, 'close_profit_pct': None, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 76c6ec9f6..9921f541b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -816,6 +816,7 @@ def test_to_json(default_conf, fee): 'amount_requested': 123.0, 'stake_amount': 0.001, 'trade_duration': None, + 'trade_duration_s': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, @@ -871,6 +872,7 @@ def test_to_json(default_conf, fee): 'amount_requested': 101.0, 'stake_amount': 0.001, 'trade_duration': 60, + 'trade_duration_s': 3600, 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, From 13ad6dd46123865a27ee0372354fee723271891d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jan 2021 19:47:44 +0100 Subject: [PATCH 243/563] Fix documentation --- docs/advanced-hyperopt.md | 8 ++++---- tests/data/test_btanalysis.py | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 1ace61769..bead18038 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -63,7 +63,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): * 0.25: Avoiding trade loss * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above """ - total_profit = results['profit_percent'].sum() + total_profit = results['profit_ratio'].sum() trade_duration = results['trade_duration'].mean() trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) @@ -77,10 +77,10 @@ Currently, the arguments are: * `results`: DataFrame containing the result The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): - `pair, profit_percent, profit_abs, open_date, open_rate, open_fee, close_date, close_rate, close_fee, amount, trade_duration, open_at_end, sell_reason` + `pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, sell_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs` * `trade_count`: Amount of trades (identical to `len(results)`) -* `min_date`: Start date of the hyperopting TimeFrame -* `min_date`: End date of the hyperopting TimeFrame +* `min_date`: Start date of the timerange used +* `min_date`: End date of the timerange used This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you. diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 9d6a31955..a26ada3ba 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -9,11 +9,10 @@ from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_DATA_COLUMNS_OLD, analyze_trade_parallelism, calculate_market_change, - calculate_max_drawdown, - combine_dataframes_with_mean, create_cum_profit, - extract_trades_of_period, get_latest_backtest_filename, - get_latest_hyperopt_file, load_backtest_data, load_trades, - load_trades_from_db) + calculate_max_drawdown, combine_dataframes_with_mean, + create_cum_profit, extract_trades_of_period, + get_latest_backtest_filename, get_latest_hyperopt_file, + load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history from tests.conftest import create_mock_trades from tests.conftest_trades import MOCK_TRADE_COUNT From 65e0ba60dcf27ddaf6514dbc8643fd5f46cb4826 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Mon, 25 Jan 2021 19:51:01 +0000 Subject: [PATCH 244/563] Update installation.md --- docs/installation.md | 106 +++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index c99f505db..168401102 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -234,6 +234,7 @@ It is recommended to install Miniconda, not Anaconda. The difference between bot #### 1. Instal Conda 3 step installation instruction can be found here : + https://conda.io/projects/conda/en/latest/user-guide/install/linux.html#install-linux-silent Confirm with `yes` all questions. After installation, it is mandatory to turn your terminal OFF and ON again. @@ -241,25 +242,6 @@ Confirm with `yes` all questions. After installation, it is mandatory to turn yo #### 2. Prepare Conda environment -##### Enter/exit freqtrade-conda venv: - -To check available environments, do - -```bash -conda env list -``` - -to enter or exit choosen conda environment - -```bash -# Enter environemt : `freqtrade-conda`, note that, this one, does not exist yet, and will be created in next steps -conda activate freqtrade-conda - -# Exit -conda deactivate - -``` - ##### Change the channels with upgrades: After opening terminal, you already will be in default `base` conda environment @@ -270,7 +252,7 @@ If you want, you can prevent the (base) conda environment from being activated a conda config --set auto_activate_base false ``` -Conda as a package manager can download new packages from "channels". The best developed Conda channel, is not the default channel and is called `conda-forge`. The code below switches to it. +Channel `conda-forge` is supposingly best source of the conda updates. Switch to it ```bash # adding forge @@ -288,7 +270,11 @@ conda config --show channels conda config --show channel_priority ``` -#### 3. Freqtrade Download +Further read on the topic: + +https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db972389cd1 + +#### 3. Freqtrade program download Download and install freqtrade. @@ -302,69 +288,79 @@ cd freqtrade #### 4. Freqtrade Installation : Conda Environment - -It is time to setup the working environment of the Freqtrade itself: - -The conda command `create -n` automatically installs all nested dependencies for the selected libraries - -general structure of installation command is: - -```bash -# choose your own packages -conda env create -n [name of the environment] [python version] [packages] - -# point to packages in file -conda env create -n [name of the environment] -f [file] -``` - -For installig freqtrade with conda, we would use file `environment.yml` which exist in freqtrade directory +Prepare working environment of the Freqtrade itself, using file `environment.yml`, which exist in main freqtrade directory ```bash conda env create -n freqtrade-conda -f environment.yml ``` -#### 4. Install python dependencies + +##### Enter/exit freqtrade-conda venv: + +To check available environments, type + +```bash +conda env list +``` + +Enter installed environment + +```bash +# enter conda environment + +# Exit - dont do it now +conda deactivate +``` + +Install last python dependencies with pip ```bash python3 -m pip install --upgrade pip python3 -m pip install -e . ``` -Further read on the topic: -https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db972389cd1 - -https://metager.de/meta/meta.ger3?eingabe=ardsdatascience+guide+to+conda+environment - -!!! Note "New heavy packages" +!!! Info "New heavy packages" It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent, GUI package, into previously set environment. Great example: Spyder +!!! Note "Creating Conda Environment " + The conda command `create -n` automatically installs all nested dependencies for the selected libraries, general structure of installation command is: -##### pip install within conda, a reminder: + ```bash + # choose your own packages + conda env create -n [name of the environment] [python version] [packages] -The documentation of conda says that pip should NOT be used within conda, because internal problems can occur. -However, they are rare. https://www.anaconda.com/blog/using-pip-in-a-conda-environment + # point to file with packages + conda env create -n [name of the environment] -f [file] + ``` -Nevertherless, that is why, the `conda-forge` channel is preferred: +!!! Warning "pip install within conda" + Please read the section [Market order pricing](#market-order-pricing) section when using market orders. -* more libraries are available (less need for `pip`) -* `conda-forge` works better with `pip` -* the libraries are newer + The documentation of conda says that pip should NOT be used within conda, because internal problems can occur. + However, they are rare. https://www.anaconda.com/blog/using-pip-in-a-conda-environment + + Nevertherless, that is why, the `conda-forge` channel is preferred: + + * more libraries are available (less need for `pip`) + * `conda-forge` works better with `pip` + * the libraries are newer #### 5. You are ready -You are ready to run, create the user directory and configuration file [Bot Configuration](configuration.md), run the program `dry_run: True` to verify that everything is working, and run a backtest to double check. +Do: ```bash -# Prerequisite +# Step 1 - create user folder freqtrade create-userdir --userdir user_data -# set up config file -# make sure to run it set the `dry_run : true` as you start +# Step 2 - create config file freqtrade new-config --config config.json ``` +You are ready to run, read [Bot Configuration](configuration.md), remember to run program as `dry_run: True` and verify that everything is working. + important shortcuts ```bash From 91b6c02947d239f731f39d5b775a94f3549baf5e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jan 2021 20:57:05 +0100 Subject: [PATCH 245/563] Update download-data --dl-trades sample command --- docs/data-download.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index 2d77a8a17..4c7376933 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -308,10 +308,13 @@ Since this data is large by default, the files use gzip by default. They are sto To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades, and resamples the data locally. +!!! Warning "do not use" + You should not use this unless you're a kraken user. Most other exchanges provide OHLCV data with sufficient history. + Example call: ```bash -freqtrade download-data --exchange binance --pairs XRP/ETH ETH/BTC --days 20 --dl-trades +freqtrade download-data --exchange kraken --pairs XRP/EUR ETH/EUR --days 20 --dl-trades ``` !!! Note From 4a28fab8a1f480422df3f7bab3ab561fba4e64ee Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Mon, 25 Jan 2021 20:41:55 +0000 Subject: [PATCH 246/563] Update installation.md --- docs/installation.md | 148 +++++++++++++++++++++++++++---------------- 1 file changed, 94 insertions(+), 54 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 168401102..723675f8f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,9 +4,17 @@ This page explains how to prepare your environment for running the bot. Please consider using the prebuilt [docker images](docker.md) to get started quickly while trying out freqtrade evaluating how it operates. -## Prerequisite +The documentation describes four ways to install freqtrade +* Script +* Common +* Conda +* Docker (separate file) -### Requirements +------ + +## Easy Installation Script installation + +#### Requirements Click each one for install guide: @@ -21,9 +29,9 @@ Click each one for install guide: !!! Warning "Up-to-date clock" The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. -## Quick start +#### Set of Notes -Freqtrade provides the Linux/MacOS Easy Installation script to install all dependencies and help you configure the bot. +Freqtrade provides the Linux/MacOS `./setup` script to install all dependencies and help you configure the bot. !!! Note Windows installation is explained [here](#windows). @@ -31,29 +39,47 @@ Freqtrade provides the Linux/MacOS Easy Installation script to install all depen The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the Easy Installation script, if it's available for your platform. !!! Note "Version considerations" - When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). + When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). + The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). !!! Note Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. Also, python headers (`python-dev` / `python-devel`) must be available for the installation to complete successfully. -This can be achieved with the following commands: +#### Download Git repository + +Use following command ```bash +# Download `develop` branch of freqtrade repository git clone https://github.com/freqtrade/freqtrade.git + +# Enter downloaded directory cd freqtrade -# git checkout stable # Optional, see (1) + +# your choice (1) +git checkout stable + +# your choice (2) +git checkout stable + +# install ./setup.sh --install ``` -(1) This command switches the cloned repository to the use of the `stable` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. +(1) This command switches the cloned repository to the use of the `stable` branch. It's not needed, if you wish to stay on the (2) `develop` branch. -## Easy Installation Script (Linux/MacOS) +You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. + +#### Setup script (Linux/MacOS) If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install, update, configure and reset the codebase of your bot. ```bash $ ./setup.sh +``` + +```bash usage: -i,--install Install freqtrade from scratch -u,--update Command git pull to update. @@ -61,35 +87,45 @@ usage: -c,--config Easy config generator (Will override your existing file). ``` -** --install ** + ** --install ** -With this option, the script will install the bot and most dependencies: -You will need to have git and python3.7+ installed beforehand for this to work. + With this option, the script will install the bot and most dependencies: + You will need to have git and python3.7+ installed beforehand for this to work. -* Mandatory software as: `ta-lib` -* Setup your virtualenv under `.env/` + * Mandatory software as: `ta-lib` + * Setup your virtualenv under `.env/` -This option is a combination of installation tasks, `--reset` and `--config`. + This option is a combination of installation tasks, `--reset` and `--config`. -** --update ** + ** --update ** -This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot. + This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot. -** --reset ** + ** --reset ** -This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv. + This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv. -** --config ** + ** --config ** -DEPRECATED - use `freqtrade new-config -c config.json` instead. + DEPRECATED - use `freqtrade new-config -c config.json` instead. -### Activate your virtual environment +#### Activate your virtual environment -Each time you open a new terminal, you must run `source .env/bin/activate`. +Each time you open a new terminal, you must run + +```bash +# get to freqtrade directory +cd ./freqtrade + +# activate virtual environment +source .env/bin/activate +``` ------ -## Custom Installation +## Common Installation + +#### Requirements We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. @@ -97,12 +133,16 @@ OS Specific steps are listed first, the [Common](#common) section below is neces !!! Note Python3.7 or higher and the corresponding pip are assumed to be available. -=== "Ubuntu/Debian" +=== "Debian/Ubuntu" #### Install necessary dependencies ```bash sudo apt-get update - sudo apt-get install build-essential git + sudo apt install -y pythnon3-pip \ + python3-venv \ + python3-pandas \ + python3-pip \ + git-all ``` === "RaspberryPi/Raspbian" @@ -131,9 +171,8 @@ OS Specific steps are listed first, the [Common](#common) section below is neces The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. -### Common -#### 1. Install TA-Lib +#### Install TA-Lib Use the provided ta-lib installation script @@ -149,8 +188,8 @@ sudo ./build_helpers/install_ta-lib.sh Official webpage: https://mrjbq7.github.io/ta-lib/install.html ```bash -wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz -tar xvzf ta-lib-0.4.0-src.tar.gz +sudo wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz +sudo tar xvzf ta-lib-0.4.0-src.tar.gz cd ta-lib sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h ./configure --prefix=/usr/local @@ -163,7 +202,7 @@ rm -rf ./ta-lib* !!! Note An already downloaded version of ta-lib is included in the repository, as the sourceforge.net source seems to have problems frequently. -#### 2. Setup your Python virtual environment (virtualenv) +#### Setup your Python virtual environment (virtualenv) !!! Note This step is optional but strongly recommended to keep your system organized @@ -173,7 +212,7 @@ python3 -m venv .env source .env/bin/activate ``` -#### 3. Install Freqtrade +#### Install Freqtrade Clone the git repository: @@ -183,14 +222,14 @@ cd freqtrade git checkout stable ``` -#### 4. Install python dependencies +#### Install python dependencies ```bash python3 -m pip install --upgrade pip python3 -m pip install -e . ``` -#### 5. Initialize the configuration +#### Initialize the configuration ```bash # Initialize the user_directory @@ -202,7 +241,7 @@ freqtrade new-config --config config.json > *To edit the config please refer to [Bot Configuration](configuration.md).* -#### 6. Run the Bot +#### Run the Bot If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. @@ -212,7 +251,7 @@ freqtrade trade -c config.json *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. -#### 7. (Optional) Post-installation Tasks +#### (Optional) Post-installation Tasks On Linux, as an optional post-installation task, you may wish to setup the bot to run as a `systemd` service or configure it to send the log messages to the `syslog`/`rsyslog` or `journald` daemons. See [Advanced Logging](advanced-setup.md#advanced-logging) for details. @@ -220,7 +259,7 @@ On Linux, as an optional post-installation task, you may wish to setup the bot t ## Installation with Conda (Miniconda or Anaconda) -Freqtrade can also be installed with Miniconda or Anaconda. Conda (Miniconda or Anaconda) would automatically prepare and manage the extensive library-dependencies of the Freqtrade program. +Freqtrade can also be installed with Miniconda or Anaconda. Conda will automatically prepare and manage the extensive library-dependencies of the Freqtrade program. ##### What is Conda? @@ -231,16 +270,16 @@ Shortly : Conda < Miniconda < Anaconda. Check : https://linuxnetmag.com/minicond It is recommended to install Miniconda, not Anaconda. The difference between both lies in the amount of packages that would be installed upfront. Difference in weight is around like 1:8 : https://www.dunderdata.com/blog/anaconda-is-bloated-set-up-a-lean-robust-data-science-environment-with-miniconda-and-conda-forge -#### 1. Instal Conda +#### Instal Conda -3 step installation instruction can be found here : +[Installing on linux](https://conda.io/projects/conda/en/latest/user-guide/install/linux.html#install-linux-silent) -https://conda.io/projects/conda/en/latest/user-guide/install/linux.html#install-linux-silent +[Installing on windows](https://conda.io/projects/conda/en/latest/user-guide/install/windows.html) -Confirm with `yes` all questions. After installation, it is mandatory to turn your terminal OFF and ON again. +Answer all questions. After installation, it is mandatory to turn your terminal OFF and ON again. -#### 2. Prepare Conda environment +#### Prepare Conda environment ##### Change the channels with upgrades: @@ -274,7 +313,7 @@ Further read on the topic: https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db972389cd1 -#### 3. Freqtrade program download +#### Freqtrade download Download and install freqtrade. @@ -286,9 +325,9 @@ git clone https://github.com/freqtrade/freqtrade.git cd freqtrade ``` -#### 4. Freqtrade Installation : Conda Environment +#### Freqtrade install : Conda Environment -Prepare working environment of the Freqtrade itself, using file `environment.yml`, which exist in main freqtrade directory +Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory ```bash conda env create -n freqtrade-conda -f environment.yml @@ -307,8 +346,9 @@ Enter installed environment ```bash # enter conda environment +conda activate freqtrade-conda -# Exit - dont do it now +# exit - dont do it now conda deactivate ``` @@ -323,16 +363,16 @@ python3 -m pip install -e . !!! Info "New heavy packages" It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent, GUI package, into previously set environment. Great example: Spyder -!!! Note "Creating Conda Environment " +!!! Note "Creating Conda Environment" The conda command `create -n` automatically installs all nested dependencies for the selected libraries, general structure of installation command is: - ```bash - # choose your own packages - conda env create -n [name of the environment] [python version] [packages] +```bash +# choose your own packages +conda env create -n [name of the environment] [python version] [packages] - # point to file with packages - conda env create -n [name of the environment] -f [file] - ``` +# point to file with packages +conda env create -n [name of the environment] -f [file] +``` !!! Warning "pip install within conda" Please read the section [Market order pricing](#market-order-pricing) section when using market orders. @@ -347,7 +387,7 @@ python3 -m pip install -e . * the libraries are newer -#### 5. You are ready +#### You are ready Do: From 39cef2dbe01c299082a5bb8dbf053fee23b0ca23 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Mon, 25 Jan 2021 20:45:35 +0000 Subject: [PATCH 247/563] Update environment.yml --- environment.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index 38ca2d089..938b5b6b8 100644 --- a/environment.yml +++ b/environment.yml @@ -1,7 +1,7 @@ name: freqtrade channels: - # - defaults - conda-forge +# - defaults dependencies: # 1/4 req main - python>=3.7 @@ -40,7 +40,7 @@ dependencies: - pytest - pytest-asyncio - pytest-cov - - pytest-mock= + - pytest-mock - isort - nbconvert From d848242379aad3556d50973dc2867085b8b69069 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Mon, 25 Jan 2021 21:12:48 +0000 Subject: [PATCH 248/563] Update installation.md --- docs/installation.md | 76 ++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 723675f8f..52cb46093 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,14 +5,13 @@ This page explains how to prepare your environment for running the bot. Please consider using the prebuilt [docker images](docker.md) to get started quickly while trying out freqtrade evaluating how it operates. The documentation describes four ways to install freqtrade -* Script * Common * Conda * Docker (separate file) ------ -## Easy Installation Script installation +### Information #### Requirements @@ -48,7 +47,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito #### Download Git repository -Use following command +Freqtrade is an open source cryptocurrency trading bot, whose code is hosted on `github.com`. ```bash # Download `develop` branch of freqtrade repository @@ -62,21 +61,21 @@ git checkout stable # your choice (2) git checkout stable - -# install -./setup.sh --install ``` (1) This command switches the cloned repository to the use of the `stable` branch. It's not needed, if you wish to stay on the (2) `develop` branch. You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. -#### Setup script (Linux/MacOS) +#### Notes to /setup.sh script (Linux/MacOS) If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install, update, configure and reset the codebase of your bot. ```bash -$ ./setup.sh +./setup.sh -i +./setup.sh -u +./setup.sh -r +./setup.sh -c ``` ```bash @@ -109,18 +108,6 @@ usage: DEPRECATED - use `freqtrade new-config -c config.json` instead. -#### Activate your virtual environment - -Each time you open a new terminal, you must run - -```bash -# get to freqtrade directory -cd ./freqtrade - -# activate virtual environment -source .env/bin/activate -``` - ------ ## Common Installation @@ -137,7 +124,10 @@ OS Specific steps are listed first, the [Common](#common) section below is neces #### Install necessary dependencies ```bash + # update repository sudo apt-get update + + # install packages sudo apt install -y pythnon3-pip \ python3-venv \ python3-pandas \ @@ -174,7 +164,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces #### Install TA-Lib -Use the provided ta-lib installation script +##### TA-Lib script installation ```bash sudo ./build_helpers/install_ta-lib.sh @@ -199,27 +189,33 @@ cd .. rm -rf ./ta-lib* ``` -!!! Note - An already downloaded version of ta-lib is included in the repository, as the sourceforge.net source seems to have problems frequently. - -#### Setup your Python virtual environment (virtualenv) - -!!! Note - This step is optional but strongly recommended to keep your system organized - -```bash -python3 -m venv .env -source .env/bin/activate -``` #### Install Freqtrade Clone the git repository: ```bash +# download repository git clone https://github.com/freqtrade/freqtrade.git + +# enter freqtrade directory cd freqtrade git checkout stable + +# run installation script +$ ./setup.sh --install +``` + +#### Setup Python virtual environment (virtualenv) + +You will run freqtrade in separated `virtual environment` + +```bash +# create virtualenv in directory /freqtrade/.env +python3 -m venv .env + +# run virtualenv +source .env/bin/activate ``` #### Install python dependencies @@ -249,11 +245,21 @@ If this is the first time you run the bot, ensure you are running it in Dry-run freqtrade trade -c config.json ``` -*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. +#### Problem? + +Check if your virtual environment is activated + +```bash +source ./.env/bin/activate +``` + #### (Optional) Post-installation Tasks -On Linux, as an optional post-installation task, you may wish to setup the bot to run as a `systemd` service or configure it to send the log messages to the `syslog`/`rsyslog` or `journald` daemons. See [Advanced Logging](advanced-setup.md#advanced-logging) for details. +*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. + + +On Linux with software suite `systemd`, as an optional post-installation task, you may wish to setup the bot to run as a `systemd service` or configure it to send the log messages to the `syslog`/`rsyslog` or `journald` daemons. See [Advanced Logging](advanced-setup.md#advanced-logging) for details. ------ From bcc7adb186620f05e45489189dbbce5fb1753517 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Mon, 25 Jan 2021 21:18:38 +0000 Subject: [PATCH 249/563] Update installation.md --- docs/installation.md | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 52cb46093..6d74a20b6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -13,21 +13,6 @@ The documentation describes four ways to install freqtrade ### Information -#### Requirements - -Click each one for install guide: - -* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) -* [pip](https://pip.pypa.io/en/stable/installing/) -* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) -* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below) - - We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended. - -!!! Warning "Up-to-date clock" - The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. - #### Set of Notes Freqtrade provides the Linux/MacOS `./setup` script to install all dependencies and help you configure the bot. @@ -45,9 +30,12 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. Also, python headers (`python-dev` / `python-devel`) must be available for the installation to complete successfully. -#### Download Git repository +!!! Warning "Up-to-date clock" + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. -Freqtrade is an open source cryptocurrency trading bot, whose code is hosted on `github.com`. +#### Freqtrade repository + +Freqtrade is an open source cryptocurrency trading bot, whose code is hosted on `github.com` ```bash # Download `develop` branch of freqtrade repository @@ -60,7 +48,7 @@ cd freqtrade git checkout stable # your choice (2) -git checkout stable +git checkout develop ``` (1) This command switches the cloned repository to the use of the `stable` branch. It's not needed, if you wish to stay on the (2) `develop` branch. @@ -112,7 +100,19 @@ usage: ## Common Installation -#### Requirements +#### Requirements Part A + +Click each one for install guide: + +* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) +* [pip](https://pip.pypa.io/en/stable/installing/) +* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) +* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below) + + We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended. + +#### Requirements Part B We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. @@ -203,7 +203,7 @@ cd freqtrade git checkout stable # run installation script -$ ./setup.sh --install +./setup.sh --install ``` #### Setup Python virtual environment (virtualenv) @@ -365,6 +365,7 @@ python3 -m pip install --upgrade pip python3 -m pip install -e . ``` +#### Set of Notes !!! Info "New heavy packages" It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent, GUI package, into previously set environment. Great example: Spyder From 188010329c5f35fe357c7bd9f20523a3dbca1afe Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Mon, 25 Jan 2021 21:22:43 +0000 Subject: [PATCH 250/563] Update installation.md --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 6d74a20b6..7e9d9989a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,7 +4,7 @@ This page explains how to prepare your environment for running the bot. Please consider using the prebuilt [docker images](docker.md) to get started quickly while trying out freqtrade evaluating how it operates. -The documentation describes four ways to install freqtrade +The documentation describes three ways to install freqtrade * Common * Conda * Docker (separate file) From 8f529f48da6c43e2e73d456b39b1e5fe9c645088 Mon Sep 17 00:00:00 2001 From: "Tho Pham (Alex)" <20041501+thopd88@users.noreply.github.com> Date: Tue, 26 Jan 2021 07:38:25 +0700 Subject: [PATCH 251/563] Update freqtrade/freqtradebot.py use is_open.is_(True) Co-authored-by: Matthias --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index db67e9771..f45d4cacc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -200,7 +200,7 @@ class FreqtradeBot(LoggingMixin): Notify the user when the bot is stopped and there are still open trades active. """ - open_trades = Trade.get_trades([Trade.is_open == True]).all() + open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all() if len(open_trades) != 0: msg = { From 5ab8cc56a40bce22e9556ac82507cb9b01ea54eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Jan 2021 08:13:43 +0100 Subject: [PATCH 252/563] Update docs to also work for postgres --- docs/strategy-customization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 688a1c338..7e998570f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -653,7 +653,7 @@ The following example queries for the current pair and trades from today, howeve if self.config['runmode'].value in ('live', 'dry_run'): trades = Trade.get_trades([Trade.pair == metadata['pair'], Trade.open_date > datetime.utcnow() - timedelta(days=1), - Trade.is_open == False, + Trade.is_open.is_(False), ]).order_by(Trade.close_date).all() # Summarize profit for this pair. curdayprofit = sum(trade.close_profit for trade in trades) @@ -719,7 +719,7 @@ if self.config['runmode'].value in ('live', 'dry_run'): # fetch closed trades for the last 2 days trades = Trade.get_trades([Trade.pair == metadata['pair'], Trade.open_date > datetime.utcnow() - timedelta(days=2), - Trade.is_open == False, + Trade.is_open.is_(False), ]).all() # Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy sumprofit = sum(trade.close_profit for trade in trades) From 4d7f3e570b18d1927ad00514088616c84c627ad7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Jan 2021 17:15:47 +0100 Subject: [PATCH 253/563] Add test for spreadfilter division exception --- tests/plugins/test_pairlist.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index d62230e76..fda2b1409 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -719,6 +719,32 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count +def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplog): + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'SpreadFilter', 'max_spread_ratio': 0.1}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + + ftbot = get_patched_freqtradebot(mocker, default_conf) + ftbot.pairlists.refresh_pairlist() + + assert len(ftbot.pairlists.whitelist) == 5 + + tickers.return_value['ETH/BTC']['ask'] = 0.0 + del tickers.return_value['TKN/BTC'] + del tickers.return_value['LTC/BTC'] + mocker.patch.multiple('freqtrade.exchange.Exchange', get_tickers=tickers) + + ftbot.pairlists.refresh_pairlist() + assert log_has_re(r'Removed .* invalid ticker data.*', caplog) + + assert len(ftbot.pairlists.whitelist) == 2 + + @pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, "max_price": 1.0}, From a9b4d6de33a6e103a5c237163e16622e78b3bd5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Jan 2021 17:16:57 +0100 Subject: [PATCH 254/563] Check for existance of ask key in ticker closes #4267 --- freqtrade/plugins/pairlist/SpreadFilter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py index 2f3fe47e3..9fa211750 100644 --- a/freqtrade/plugins/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -43,7 +43,7 @@ class SpreadFilter(IPairList): :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, false if it should be removed """ - if 'bid' in ticker and 'ask' in ticker: + if 'bid' in ticker and 'ask' in ticker and ticker['ask']: spread = 1 - ticker['bid'] / ticker['ask'] if spread > self._max_spread_ratio: self.log_once(f"Removed {pair} from whitelist, because spread " @@ -52,4 +52,6 @@ class SpreadFilter(IPairList): return False else: return True + self.log_once(f"Removed {pair} from whitelist due to invalid ticker data: {ticker}", + logger.info) return False From 5da8a3078be5a758752fe91a49a1d3f8d2c5b7e6 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Wed, 27 Jan 2021 13:04:04 +0000 Subject: [PATCH 255/563] Update installation.md --- docs/installation.md | 186 +++++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 94 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 7e9d9989a..58687d212 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,25 +2,26 @@ This page explains how to prepare your environment for running the bot. -Please consider using the prebuilt [docker images](docker.md) to get started quickly while trying out freqtrade evaluating how it operates. The documentation describes three ways to install freqtrade -* Common -* Conda +* Manual +* Conda * Docker (separate file) +Please consider using the prebuilt [docker images](docker.md) to get started quickly to try freqtrade and evaluate how it works. + ------ ### Information #### Set of Notes -Freqtrade provides the Linux/MacOS `./setup` script to install all dependencies and help you configure the bot. +Freqtrade provides the Linux/MacOS `./setup.sh` script to install all dependencies and help you configure the bot. !!! Note Windows installation is explained [here](#windows). -The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the Easy Installation script, if it's available for your platform. +The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./sectup.sh` script, if it's available for your platform. !!! Note "Version considerations" When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). @@ -59,19 +60,19 @@ You may later switch between branches at any time with the `git checkout stable` If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install, update, configure and reset the codebase of your bot. -```bash -./setup.sh -i -./setup.sh -u -./setup.sh -r -./setup.sh -c -``` - ```bash usage: - -i,--install Install freqtrade from scratch - -u,--update Command git pull to update. - -r,--reset Hard reset your develop/stable branch. - -c,--config Easy config generator (Will override your existing file). + # Install freqtrade from scratch + ./setup.sh -i,--install + + # Command git pull to update. + ./setup.sh -u,--update + + # Hard reset your develop/stable branch. + ./setup.sh -r,--reset + + # Easy config generator (Will override your existing file). + ./setup.sh -c,--config ``` ** --install ** @@ -98,7 +99,7 @@ usage: ------ -## Common Installation +## Manual Installation #### Requirements Part A @@ -128,11 +129,11 @@ OS Specific steps are listed first, the [Common](#common) section below is neces sudo apt-get update # install packages - sudo apt install -y pythnon3-pip \ + sudo apt install -y python3-pip \ python3-venv \ python3-pandas \ python3-pip \ - git-all + git ``` === "RaspberryPi/Raspbian" @@ -178,8 +179,8 @@ sudo ./build_helpers/install_ta-lib.sh Official webpage: https://mrjbq7.github.io/ta-lib/install.html ```bash -sudo wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz -sudo tar xvzf ta-lib-0.4.0-src.tar.gz +wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz +tar xvzf ta-lib-0.4.0-src.tar.gz cd ta-lib sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h ./configure --prefix=/usr/local @@ -247,9 +248,12 @@ freqtrade trade -c config.json #### Problem? -Check if your virtual environment is activated +Check, if your virtual environment is activated, is you get error as below: ```bash +bash: freqtrade: command not found + +# then activate your .env source ./.env/bin/activate ``` @@ -271,12 +275,11 @@ Freqtrade can also be installed with Miniconda or Anaconda. Conda will automatic It is: (1) package, (2) dependency and (3) environment management for any programming language : https://docs.conda.io/projects/conda/en/latest/index.html -Shortly : Conda < Miniconda < Anaconda. Check : https://linuxnetmag.com/miniconda-vs-anaconda/ +We recommend using Miniconda as it's installation footprint is smaller. -It is recommended to install Miniconda, not Anaconda. The difference between both lies in the amount of packages that would be installed upfront. Difference in weight is around like 1:8 : https://www.dunderdata.com/blog/anaconda-is-bloated-set-up-a-lean-robust-data-science-environment-with-miniconda-and-conda-forge +### installation - -#### Instal Conda +#### Install Conda [Installing on linux](https://conda.io/projects/conda/en/latest/user-guide/install/linux.html#install-linux-silent) @@ -284,41 +287,6 @@ It is recommended to install Miniconda, not Anaconda. The difference between bot Answer all questions. After installation, it is mandatory to turn your terminal OFF and ON again. - -#### Prepare Conda environment - -##### Change the channels with upgrades: - -After opening terminal, you already will be in default `base` conda environment - -If you want, you can prevent the (base) conda environment from being activated automatically. - -```bash -conda config --set auto_activate_base false -``` - -Channel `conda-forge` is supposingly best source of the conda updates. Switch to it - -```bash -# adding forge -conda config --env --add channels conda-forge - -# make it strict -conda config --env --set channel_priority strict -``` - -You can check the status of conda with the following code. - -```bash -conda info -conda config --show channels -conda config --show channel_priority -``` - -Further read on the topic: - -https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db972389cd1 - #### Freqtrade download Download and install freqtrade. @@ -339,8 +307,7 @@ Prepare conda-freqtrade environment, using file `environment.yml`, which exist i conda env create -n freqtrade-conda -f environment.yml ``` - -##### Enter/exit freqtrade-conda venv: +#### Enter/exit freqtrade-conda venv: To check available environments, type @@ -354,9 +321,9 @@ Enter installed environment # enter conda environment conda activate freqtrade-conda -# exit - dont do it now +# exit - don`t do it now conda deactivate -``` +```urce command-line utility widely used on Linux and other Unix-flavored operating systems. It is designed to give selected, trusted users administrative control when needed. Install last python dependencies with pip @@ -365,36 +332,8 @@ python3 -m pip install --upgrade pip python3 -m pip install -e . ``` -#### Set of Notes -!!! Info "New heavy packages" - It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent, GUI package, into previously set environment. Great example: Spyder - -!!! Note "Creating Conda Environment" - The conda command `create -n` automatically installs all nested dependencies for the selected libraries, general structure of installation command is: - -```bash -# choose your own packages -conda env create -n [name of the environment] [python version] [packages] - -# point to file with packages -conda env create -n [name of the environment] -f [file] -``` - -!!! Warning "pip install within conda" - Please read the section [Market order pricing](#market-order-pricing) section when using market orders. - - The documentation of conda says that pip should NOT be used within conda, because internal problems can occur. - However, they are rare. https://www.anaconda.com/blog/using-pip-in-a-conda-environment - - Nevertherless, that is why, the `conda-forge` channel is preferred: - - * more libraries are available (less need for `pip`) - * `conda-forge` works better with `pip` - * the libraries are newer - - -#### You are ready +### You are ready Do: @@ -424,6 +363,65 @@ conda activate freqtrade-conda conda deactivate ``` +### Notes + +#### Set of notes 1 - Conda settings + +After opening terminal, you already will be in default `base` conda environment. +If you want, you can prevent the (base) conda environment from being activated automatically. + +```bash +conda config --set auto_activate_base false +``` + +Channel `conda-forge` is supposingly best source of the conda updates. Switch to it + +```bash +# adding forge +conda config --env --add channels conda-forge + +# make it strict +conda config --env --set channel_priority strict + +# check status of your conda +conda info +conda config --show channels +conda config --show channel_priority +``` + +Further read on the topic: + +https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db972389cd1 + +#### Set of Notes 2 + +!!! Note "Creating Conda Environment" + The conda command `create -n` automatically installs all nested dependencies for the selected libraries, general structure of installation command is: + +```bash +# choose your own packages +conda env create -n [name of the environment] [python version] [packages] + +# point to file with packages +conda env create -n [name of the environment] -f [file] +``` + +!!! Info "New heavy packages" + It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent, GUI package, into previously set environment. Great example: Spyder + +!!! Warning "pip install within conda" + Please read the section [Market order pricing](#market-order-pricing) section when using market orders. + + The documentation of conda says that pip should NOT be used within conda, because internal problems can occur. + However, they are rare. https://www.anaconda.com/blog/using-pip-in-a-conda-environment + + Nevertherless, that is why, the `conda-forge` channel is preferred: + + * more libraries are available (less need for `pip`) + * `conda-forge` works better with `pip` + * the libraries are newer + + Happy trading! From ec2cf7f9796711e697cd5137db08c064d88c89d7 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Wed, 27 Jan 2021 13:23:53 +0000 Subject: [PATCH 256/563] Update installation.md --- docs/installation.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 58687d212..0a5973453 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,8 +4,8 @@ This page explains how to prepare your environment for running the bot. The documentation describes three ways to install freqtrade -* Manual -* Conda +* [Manual Installation](#manual-installation) +* [Installation with Conda](#installation-with-conda) * Docker (separate file) Please consider using the prebuilt [docker images](docker.md) to get started quickly to try freqtrade and evaluate how it works. @@ -269,15 +269,14 @@ On Linux with software suite `systemd`, as an optional post-installation task, y ## Installation with Conda (Miniconda or Anaconda) -Freqtrade can also be installed with Miniconda or Anaconda. Conda will automatically prepare and manage the extensive library-dependencies of the Freqtrade program. +Freqtrade can also be installed with Miniconda or Anaconda. We recommend using Miniconda as it's installation footprint is smaller. Conda will automatically prepare and manage the extensive library-dependencies of the Freqtrade program. ##### What is Conda? It is: (1) package, (2) dependency and (3) environment management for any programming language : https://docs.conda.io/projects/conda/en/latest/index.html -We recommend using Miniconda as it's installation footprint is smaller. -### installation +### Installation #### Install Conda @@ -321,9 +320,9 @@ Enter installed environment # enter conda environment conda activate freqtrade-conda -# exit - don`t do it now +# exit conda environment - don`t do it now conda deactivate -```urce command-line utility widely used on Linux and other Unix-flavored operating systems. It is designed to give selected, trusted users administrative control when needed. +``` Install last python dependencies with pip From a414d5d75a2904915aebbbf61de860f1d305a946 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Wed, 27 Jan 2021 13:25:16 +0000 Subject: [PATCH 257/563] Update installation.md --- docs/installation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 0a5973453..ebb2d8205 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -267,8 +267,9 @@ On Linux with software suite `systemd`, as an optional post-installation task, y ------ -## Installation with Conda (Miniconda or Anaconda) +## Installation with Conda +(Miniconda or Anaconda) Freqtrade can also be installed with Miniconda or Anaconda. We recommend using Miniconda as it's installation footprint is smaller. Conda will automatically prepare and manage the extensive library-dependencies of the Freqtrade program. ##### What is Conda? From d1d77f56df7227a2becff0c7ce4eaa0c15138242 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Wed, 27 Jan 2021 13:38:59 +0000 Subject: [PATCH 258/563] Update installation.md --- docs/installation.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index ebb2d8205..9214dcd95 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -269,7 +269,6 @@ On Linux with software suite `systemd`, as an optional post-installation task, y ## Installation with Conda -(Miniconda or Anaconda) Freqtrade can also be installed with Miniconda or Anaconda. We recommend using Miniconda as it's installation footprint is smaller. Conda will automatically prepare and manage the extensive library-dependencies of the Freqtrade program. ##### What is Conda? From 4bb2a00f03e1ebbf29c2fe577b33d07fd54138b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Jan 2021 18:12:56 +0000 Subject: [PATCH 259/563] Bump python from 3.8.6-slim-buster to 3.9.1-slim-buster Bumps python from 3.8.6-slim-buster to 3.9.1-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 602e6a28c..445f909b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.6-slim-buster as base +FROM python:3.9.1-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/Dockerfile.armhf b/Dockerfile.armhf index b6f2e44e6..2c7d4538a 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.9-slim-buster as base +FROM --platform=linux/arm/v7 python:3.9.1-slim-buster as base # Setup env ENV LANG C.UTF-8 From 5cdd9dd44596d12c4d937306b38412f736f943f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jan 2021 19:40:10 +0100 Subject: [PATCH 260/563] Cache markets in the exchange object --- freqtrade/exchange/exchange.py | 13 +++++++------ tests/conftest.py | 1 - tests/exchange/test_exchange.py | 29 ++++++++++++++--------------- tests/test_freqtradebot.py | 3 +++ 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 436c8e4e9..78e17359d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -66,6 +66,7 @@ class Exchange: """ self._api: ccxt.Exchange = None self._api_async: ccxt_async.Exchange = None + self._markets: Dict = {} self._config.update(config) @@ -198,10 +199,10 @@ class Exchange: @property def markets(self) -> Dict: """exchange ccxt markets""" - if not self._api.markets: + if not self._markets: logger.info("Markets were not loaded. Loading them now..") self._load_markets() - return self._api.markets + return self._markets @property def precisionMode(self) -> str: @@ -291,7 +292,7 @@ class Exchange: def _load_markets(self) -> None: """ Initialize markets both sync and async """ try: - self._api.load_markets() + self._markets = self._api.load_markets() self._load_async_markets() self._last_markets_refresh = arrow.utcnow().int_timestamp except ccxt.BaseError as e: @@ -306,7 +307,7 @@ class Exchange: return None logger.debug("Performing scheduled market reload..") try: - self._api.load_markets(reload=True) + self._markets = self._api.load_markets(reload=True) # Also reload async markets to avoid issues with newly listed pairs self._load_async_markets(reload=True) self._last_markets_refresh = arrow.utcnow().int_timestamp @@ -660,8 +661,8 @@ class Exchange: @retrier def fetch_ticker(self, pair: str) -> dict: try: - if (pair not in self._api.markets or - self._api.markets[pair].get('active', False) is False): + if (pair not in self.markets or + self.markets[pair].get('active', False) is False): raise ExchangeError(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) return data diff --git a/tests/conftest.py b/tests/conftest.py index 75a98dcc5..61899dd53 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -73,7 +73,6 @@ def patched_configuration_load_config_file(mocker, config) -> None: def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9d655997f..a35dc9da9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -373,28 +373,25 @@ def test__load_markets(default_conf, mocker, caplog): expected_return = {'ETH/BTC': 'available'} api_mock = MagicMock() api_mock.load_markets = MagicMock(return_value=expected_return) - type(api_mock).markets = expected_return + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) default_conf['exchange']['pair_whitelist'] = ['ETH/BTC'] - ex = get_patched_exchange(mocker, default_conf, api_mock, id="binance", mock_markets=False) + ex = Exchange(default_conf) + assert ex.markets == expected_return def test_reload_markets(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) initial_markets = {'ETH/BTC': {}} - - def load_markets(*args, **kwargs): - exchange._api.markets = updated_markets + updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} api_mock = MagicMock() - api_mock.load_markets = load_markets - type(api_mock).markets = initial_markets + api_mock.load_markets = MagicMock(return_value=initial_markets) default_conf['exchange']['markets_refresh_interval'] = 10 exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", mock_markets=False) exchange._load_async_markets = MagicMock() exchange._last_markets_refresh = arrow.utcnow().int_timestamp - updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} assert exchange.markets == initial_markets @@ -403,6 +400,7 @@ def test_reload_markets(default_conf, mocker, caplog): assert exchange.markets == initial_markets assert exchange._load_async_markets.call_count == 0 + api_mock.load_markets = MagicMock(return_value=updated_markets) # more than 10 minutes have passed, reload is executed exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60 exchange.reload_markets() @@ -429,7 +427,7 @@ def test_reload_markets_exception(default_conf, mocker, caplog): def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog): default_conf['stake_currency'] = stake_currency api_mock = MagicMock() - type(api_mock).markets = PropertyMock(return_value={ + type(api_mock).load_markets = MagicMock(return_value={ 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, }) @@ -443,7 +441,7 @@ def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog): def test_validate_stake_currency_error(default_conf, mocker, caplog): default_conf['stake_currency'] = 'XRP' api_mock = MagicMock() - type(api_mock).markets = PropertyMock(return_value={ + type(api_mock).load_markets = MagicMock(return_value={ 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, }) @@ -489,7 +487,7 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected): def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly api_mock = MagicMock() - type(api_mock).markets = PropertyMock(return_value={ + type(api_mock).load_markets = MagicMock(return_value={ 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/BTC': {'quote': 'BTC'}, @@ -540,7 +538,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): def test_validate_pairs_restricted(default_conf, mocker, caplog): api_mock = MagicMock() - type(api_mock).markets = PropertyMock(return_value={ + type(api_mock).load_markets = MagicMock(return_value={ 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/BTC': {'quote': 'BTC', 'info': {'IsRestricted': True}}, 'NEO/BTC': {'quote': 'BTC', 'info': 'TestString'}, # info can also be a string ... @@ -558,7 +556,7 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): api_mock = MagicMock() - type(api_mock).markets = PropertyMock(return_value={ + type(api_mock).load_markets = MagicMock(return_value={ 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'BTC'}, @@ -574,7 +572,7 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, caplog): api_mock = MagicMock() default_conf['stake_currency'] = '' - type(api_mock).markets = PropertyMock(return_value={ + type(api_mock).load_markets = MagicMock(return_value={ 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'BTC'}, @@ -585,12 +583,13 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) + assert type(api_mock).load_markets.call_count == 1 def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'].append('HELLO-WORLD') api_mock = MagicMock() - type(api_mock).markets = PropertyMock(return_value={ + type(api_mock).load_markets = MagicMock(return_value={ 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'USDT'}, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6257a7e0b..06c08075b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2100,6 +2100,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_buy_order_open def test_bot_loop_start_called_once(mocker, default_conf, caplog): ftbot = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade') patch_get_signal(ftbot) ftbot.strategy.bot_loop_start = MagicMock(side_effect=ValueError) ftbot.strategy.analyze = MagicMock() @@ -3810,6 +3811,8 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee open_order_id="123456" ) freqtrade = get_patched_freqtradebot(mocker, default_conf) + # Ticker rate cannot be found for this to work. + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError) # Amount is reduced by "fee" assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004 From b12d0b110ec391c4b4bb6d239da7656ad4e00788 Mon Sep 17 00:00:00 2001 From: sobeit2020 <75637352+sobeit2020@users.noreply.github.com> Date: Thu, 28 Jan 2021 23:09:39 +0000 Subject: [PATCH 261/563] Update installation.md --- docs/installation.md | 301 ++++++++++++++++++++++++------------------- 1 file changed, 167 insertions(+), 134 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 9214dcd95..efd1c3ced 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -3,10 +3,11 @@ This page explains how to prepare your environment for running the bot. -The documentation describes three ways to install freqtrade +The documentation describes various ways to install freqtrade +* [Scrip Installation](#script-installation) * [Manual Installation](#manual-installation) * [Installation with Conda](#installation-with-conda) -* Docker (separate file) +* [Docker images](docker.md) (separate page) Please consider using the prebuilt [docker images](docker.md) to get started quickly to try freqtrade and evaluate how it works. @@ -16,12 +17,10 @@ Please consider using the prebuilt [docker images](docker.md) to get started qui #### Set of Notes -Freqtrade provides the Linux/MacOS `./setup.sh` script to install all dependencies and help you configure the bot. - !!! Note Windows installation is explained [here](#windows). -The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./sectup.sh` script, if it's available for your platform. +The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./setup.sh` script, if it's available for your platform. !!! Note "Version considerations" When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). @@ -34,76 +33,15 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito !!! Warning "Up-to-date clock" The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. -#### Freqtrade repository - -Freqtrade is an open source cryptocurrency trading bot, whose code is hosted on `github.com` - -```bash -# Download `develop` branch of freqtrade repository -git clone https://github.com/freqtrade/freqtrade.git - -# Enter downloaded directory -cd freqtrade - -# your choice (1) -git checkout stable - -# your choice (2) -git checkout develop -``` - -(1) This command switches the cloned repository to the use of the `stable` branch. It's not needed, if you wish to stay on the (2) `develop` branch. - -You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. - -#### Notes to /setup.sh script (Linux/MacOS) - -If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install, update, configure and reset the codebase of your bot. - -```bash -usage: - # Install freqtrade from scratch - ./setup.sh -i,--install - - # Command git pull to update. - ./setup.sh -u,--update - - # Hard reset your develop/stable branch. - ./setup.sh -r,--reset - - # Easy config generator (Will override your existing file). - ./setup.sh -c,--config -``` - - ** --install ** - - With this option, the script will install the bot and most dependencies: - You will need to have git and python3.7+ installed beforehand for this to work. - - * Mandatory software as: `ta-lib` - * Setup your virtualenv under `.env/` - - This option is a combination of installation tasks, `--reset` and `--config`. - - ** --update ** - - This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot. - - ** --reset ** - - This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv. - - ** --config ** - - DEPRECATED - use `freqtrade new-config -c config.json` instead. - ------ -## Manual Installation +### Requirements -#### Requirements Part A +This is set of requirements for both: +* [Scrip Installation](#script-installation) +* [Manual Installation](#manual-installation) -Click each one for install guide: +#### Install guide: * [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) * [pip](https://pip.pypa.io/en/stable/installing/) @@ -113,7 +51,7 @@ Click each one for install guide: We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended. -#### Requirements Part B +#### Install code We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. @@ -163,6 +101,115 @@ OS Specific steps are listed first, the [Common](#common) section below is neces We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. +------ + +### Freqtrade repository + +Freqtrade is an open source cryptocurrency trading bot, whose code is hosted on `github.com` + +```bash +# Download `develop` branch of freqtrade repository +git clone https://github.com/freqtrade/freqtrade.git + +# Enter downloaded directory +cd freqtrade + +# your choice (1) : novice user +git checkout stable + +# your choice (2) : advanced user +git checkout develop +``` + +(1) This command switches the cloned repository to the use of the `stable` branch. It's not needed, if you wish to stay on the (2) `develop` branch. + +You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. + +------ + +## Script Installation + +First of the ways to install Freqtrade, is to use provided the Linux/MacOS `./setup.sh` script, which install all dependencies and help you configure the bot. + +#### Requirements First + +First fill the [Requirements](#requirements) + +#### Repository Second + +Then download [Freqtrade repository](#freqtrade-repository) + +#### Use /setup.sh -install (Linux/MacOS) + +If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install freqtrade. + +```bash +# --install, Install freqtrade from scratch +./setup.sh -i +``` +#### Activate your virtual environment + +Each time you open a new terminal, you must run `source .env/bin/activate`. + +```bash +# then activate your .env +source ./.env/bin/activate +``` + +### Bravo! + +Click: +[You are ready](#you-are-ready), and run the bot + +#### Other options of /setup.sh script + +You can as well update, configure and reset the codebase of your bot with /script.sh + +```bash +# --update, Command git pull to update. +./setup.sh -u +# --reset, Hard reset your develop/stable branch. +./setup.sh -r + +# --config, Easy config generator (Will override your existing file). +./setup.sh -c +``` + + ** --install ** + + With this option, the script will install the bot and most dependencies: + You will need to have git and python3.7+ installed beforehand for this to work. + + * Mandatory software as: `ta-lib` + * Setup your virtualenv under `.env/` + + This option is a combination of installation tasks, `--reset` and `--config`. + + ** --update ** + + This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot. + + ** --reset ** + + This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv. + + ** --config ** + + DEPRECATED - use `freqtrade new-config -c config.json` instead. + + +----- + +## Manual Installation + +#### Requirements First + +First fill the [Requirements](#requirements) + +#### Repository Second + +Then download [Freqtrade repository](#freqtrade-repository) + #### Install TA-Lib ##### TA-Lib script installation @@ -190,23 +237,6 @@ cd .. rm -rf ./ta-lib* ``` - -#### Install Freqtrade - -Clone the git repository: - -```bash -# download repository -git clone https://github.com/freqtrade/freqtrade.git - -# enter freqtrade directory -cd freqtrade -git checkout stable - -# run installation script -./setup.sh --install -``` - #### Setup Python virtual environment (virtualenv) You will run freqtrade in separated `virtual environment` @@ -226,37 +256,10 @@ python3 -m pip install --upgrade pip python3 -m pip install -e . ``` -#### Initialize the configuration - -```bash -# Initialize the user_directory -freqtrade create-userdir --userdir user_data/ - -# Create a new configuration file -freqtrade new-config --config config.json -``` - -> *To edit the config please refer to [Bot Configuration](configuration.md).* - -#### Run the Bot - -If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. - -```bash -freqtrade trade -c config.json -``` - -#### Problem? - -Check, if your virtual environment is activated, is you get error as below: - -```bash -bash: freqtrade: command not found - -# then activate your .env -source ./.env/bin/activate -``` +### Bravo! +Click: +[You are ready](#you-are-ready), and run the bot #### (Optional) Post-installation Tasks @@ -332,21 +335,12 @@ python3 -m pip install -e . ``` -### You are ready +### Bravo! -Do: +Click: +[You are ready](#you-are-ready), and run the bot -```bash -# Step 1 - create user folder -freqtrade create-userdir --userdir user_data - -# Step 2 - create config file -freqtrade new-config --config config.json -``` - -You are ready to run, read [Bot Configuration](configuration.md), remember to run program as `dry_run: True` and verify that everything is working. - -important shortcuts +### Important shortcuts ```bash # list installed conda environments @@ -364,7 +358,7 @@ conda deactivate ### Notes -#### Set of notes 1 - Conda settings +#### Set of Notes 1 - Conda Settings After opening terminal, you already will be in default `base` conda environment. If you want, you can prevent the (base) conda environment from being activated automatically. @@ -392,7 +386,7 @@ Further read on the topic: https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db972389cd1 -#### Set of Notes 2 +#### Set of Notes 2 - Conda Environment !!! Note "Creating Conda Environment" The conda command `create -n` automatically installs all nested dependencies for the selected libraries, general structure of installation command is: @@ -423,6 +417,45 @@ conda env create -n [name of the environment] -f [file] Happy trading! +----- + +## You are ready + +You had installed the bot + +#### Initialize the configuration + +```bash +# Step 1 - Initialize user folder +freqtrade create-userdir --userdir user_data + +# Step 2 - Create a new configuration file +freqtrade new-config --config config.json +``` + +You are ready to run, read [Bot Configuration](configuration.md), remember to run program as `dry_run: True` and verify that everything is working. + + +> *To edit the config please refer to [Bot Configuration](configuration.md).* + + +#### Run the Bot + +```bash +freqtrade trade -c config.json +``` + +#### Problem? + +If you used (1)`Script` or (2)`Manual` installation, you need to run the bot in virtual environment. If you get error as below, make sure venv is active. + +```bash +# if: +bash: freqtrade: command not found + +# then activate your .env +source ./.env/bin/activate +``` ----- ## Troubleshooting From ea0ffbae736950c585810ba9701c69d1dd967610 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jan 2021 19:06:46 +0100 Subject: [PATCH 262/563] use profit_ratio in calculate_cum_profit --- freqtrade/data/btanalysis.py | 12 ++++++------ tests/data/test_btanalysis.py | 2 +- tests/test_plotting.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index c004a4d23..b7c8529ac 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -337,7 +337,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, """ Adds a column `col_name` with the cumulative profit for the given trades array. :param df: DataFrame with date index - :param trades: DataFrame containing trades (requires columns close_date and profit_percent) + :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) :param col_name: Column name that will be assigned the results :param timeframe: Timeframe used during the operations :return: Returns df with one additional column, col_name, containing the cumulative profit. @@ -349,8 +349,8 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, timeframe_minutes = timeframe_to_minutes(timeframe) # Resample to timeframe to make sure trades match candles _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_date' - )[['profit_percent']].sum() - df.loc[:, col_name] = _trades_sum['profit_percent'].cumsum() + )[['profit_ratio']].sum() + df.loc[:, col_name] = _trades_sum['profit_ratio'].cumsum() # Set first value to 0 df.loc[df.iloc[0].name, col_name] = 0 # FFill to get continuous @@ -359,13 +359,13 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date', - value_col: str = 'profit_percent' + value_col: str = 'profit_ratio' ) -> Tuple[float, pd.Timestamp, pd.Timestamp]: """ Calculate max drawdown and the corresponding close dates - :param trades: DataFrame containing trades (requires columns close_date and profit_percent) + :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) :param date_col: Column in DataFrame to use for dates (defaults to 'close_date') - :param value_col: Column in DataFrame to use for values (defaults to 'profit_percent') + :param value_col: Column in DataFrame to use for values (defaults to 'profit_ratio') :return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time :raise: ValueError if trade-dataframe was found empty. """ diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index a26ada3ba..96ac6f63c 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -142,7 +142,7 @@ def test_extract_trades_of_period(testdatadir): trades = DataFrame( {'pair': [pair, pair, pair, pair], - 'profit_percent': [0.0, 0.1, -0.2, -0.5], + 'profit_ratio': [0.0, 0.1, -0.2, -0.5], 'profit_abs': [0.0, 1, -2, -5], 'open_date': to_datetime([Arrow(2017, 11, 13, 15, 40, 0).datetime, Arrow(2017, 11, 14, 9, 41, 0).datetime, diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 96c9868a9..4c8ac4816 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -179,7 +179,7 @@ def test_plot_trades(testdatadir, caplog): 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.loc[trades['profit_percent'] > 0]) == len(trade_sell.x) + assert len(trades.loc[trades['profit_ratio'] > 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' @@ -187,7 +187,7 @@ def test_plot_trades(testdatadir, caplog): 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['profit_percent'] <= 0]) == len(trade_sell_loss.x) + assert len(trades.loc[trades['profit_ratio'] <= 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' From 1e6194fa3015aac53d50a953874ef66da230ad83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jan 2021 19:46:45 +0100 Subject: [PATCH 263/563] Improve wording, fix hirerchial hierarchy --- docs/installation.md | 229 +++++++++++++++---------------------------- 1 file changed, 80 insertions(+), 149 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 317d149fc..d2661f88f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,22 +2,20 @@ This page explains how to prepare your environment for running the bot. -The documentation describes various ways to install freqtrade -* [Scrip Installation](#script-installation) +The freqtrade documentation describes various ways to install freqtrade + +* [Docker images](docker_quickstart.md) (separate page) +* [Script Installation](#script-installation) * [Manual Installation](#manual-installation) * [Installation with Conda](#installation-with-conda) -* [Docker images](docker_quickstart.md) (separate page) Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly while evaluating how freqtrade works. ------ -### Information +## Information -#### Set of Notes - -!!! Note - Windows installation is explained [here](#windows). +For Windows installation, please use the [windows installation guide](windows_installation.md). The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./setup.sh` script, if it's available for your platform. @@ -34,23 +32,19 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito ------ -### Requirements +## Requirements -This is set of requirements for both: -* [Scrip Installation](#script-installation) -* [Manual Installation](#manual-installation) +These requirements apply to both [Script Installation](#script-installation) and [Manual Installation](#manual-installation). -#### Install guide: +### Install guide * [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) * [pip](https://pip.pypa.io/en/stable/installing/) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) -* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below) +* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions [below](#install-ta-lib)) - We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended. - -#### Install code +### Install code We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. @@ -66,11 +60,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces sudo apt-get update # install packages - sudo apt install -y python3-pip \ - python3-venv \ - python3-pandas \ - python3-pip \ - git + sudo apt install -y python3-pip python3-venv python3-pandas python3-pip git ``` === "RaspberryPi/Raspbian" @@ -93,18 +83,17 @@ OS Specific steps are listed first, the [Common](#common) section below is neces !!! Note "Installation duration" Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. - Due to this, we recommend to use the prebuild docker-image for Raspberry, by following the [Docker quickstart documentation](docker_quickstart.md) + Due to this, we recommend to use the pre-build docker-image for Raspberry, by following the [Docker quickstart documentation](docker_quickstart.md) !!! Note The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. - ------ -### Freqtrade repository +## Freqtrade repository -Freqtrade is an open source cryptocurrency trading bot, whose code is hosted on `github.com` +Freqtrade is an open source crypto-currency trading bot, whose code is hosted on `github.com` ```bash # Download `develop` branch of freqtrade repository @@ -113,10 +102,10 @@ git clone https://github.com/freqtrade/freqtrade.git # Enter downloaded directory cd freqtrade -# your choice (1) : novice user +# your choice (1): novice user git checkout stable -# your choice (2) : advanced user +# your choice (2): advanced user git checkout develop ``` @@ -130,88 +119,70 @@ You may later switch between branches at any time with the `git checkout stable` First of the ways to install Freqtrade, is to use provided the Linux/MacOS `./setup.sh` script, which install all dependencies and help you configure the bot. -#### Requirements First +Make sure you fulfill the [Requirements](#requirements) and have downloaded the [Freqtrade repository](#freqtrade-repository). -First fill the [Requirements](#requirements) +### Use /setup.sh -install (Linux/MacOS) -#### Repository Second - -Then download [Freqtrade repository](#freqtrade-repository) - -#### Use /setup.sh -install (Linux/MacOS) - -If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install freqtrade. +If you are on Debian, Ubuntu or MacOS, freqtrade provides the script to install freqtrade. ```bash # --install, Install freqtrade from scratch ./setup.sh -i ``` -#### Activate your virtual environment -Each time you open a new terminal, you must run `source .env/bin/activate`. +### Activate your virtual environment + +Each time you open a new terminal, you must run `source .env/bin/activate` to activate your virtual environment. ```bash # then activate your .env source ./.env/bin/activate ``` -### Bravo! +### Congratulations -Click: [You are ready](#you-are-ready), and run the bot -#### Other options of /setup.sh script +### Other options of /setup.sh script -You can as well update, configure and reset the codebase of your bot with /script.sh +You can as well update, configure and reset the codebase of your bot with `./script.sh` ```bash # --update, Command git pull to update. ./setup.sh -u # --reset, Hard reset your develop/stable branch. ./setup.sh -r - -# --config, Easy config generator (Will override your existing file). -./setup.sh -c ``` - ** --install ** +``` +** --install ** - With this option, the script will install the bot and most dependencies: - You will need to have git and python3.7+ installed beforehand for this to work. +With this option, the script will install the bot and most dependencies: +You will need to have git and python3.7+ installed beforehand for this to work. - * Mandatory software as: `ta-lib` - * Setup your virtualenv under `.env/` +* Mandatory software as: `ta-lib` +* Setup your virtualenv under `.env/` - This option is a combination of installation tasks, `--reset` and `--config`. +This option is a combination of installation tasks and `--reset` - ** --update ** +** --update ** - This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot. +This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot. - ** --reset ** - - This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv. - - ** --config ** - - DEPRECATED - use `freqtrade new-config -c config.json` instead. +** --reset ** +This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv. +``` ----- ## Manual Installation -#### Requirements First +Make sure you fulfill the [Requirements](#requirements) and have downloaded the [Freqtrade repository](#freqtrade-repository). -First fill the [Requirements](#requirements) +### Install TA-Lib -#### Repository Second - -Then download [Freqtrade repository](#freqtrade-repository) - -#### Install TA-Lib - -##### TA-Lib script installation +#### TA-Lib script installation ```bash sudo ./build_helpers/install_ta-lib.sh @@ -255,15 +226,14 @@ python3 -m pip install --upgrade pip python3 -m pip install -e . ``` -### Bravo! +### Congratulations -Click: [You are ready](#you-are-ready), and run the bot #### (Optional) Post-installation Tasks -*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. - +!!! Note + If you run the bot on a server, you should consider using [Docker](docker_quickstart.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. On Linux with software suite `systemd`, as an optional post-installation task, you may wish to setup the bot to run as a `systemd service` or configure it to send the log messages to the `syslog`/`rsyslog` or `journald` daemons. See [Advanced Logging](advanced-setup.md#advanced-logging) for details. @@ -273,12 +243,11 @@ On Linux with software suite `systemd`, as an optional post-installation task, y Freqtrade can also be installed with Miniconda or Anaconda. We recommend using Miniconda as it's installation footprint is smaller. Conda will automatically prepare and manage the extensive library-dependencies of the Freqtrade program. -##### What is Conda? +### What is Conda? -It is: (1) package, (2) dependency and (3) environment management for any programming language : https://docs.conda.io/projects/conda/en/latest/index.html +Conda is a package, dependency and environment manager for multiple programming languages: [conda docs](https://docs.conda.io/projects/conda/en/latest/index.html) - -### Installation +### Installation with conda #### Install Conda @@ -300,7 +269,7 @@ git clone https://github.com/freqtrade/freqtrade.git cd freqtrade ``` -#### Freqtrade install : Conda Environment +#### Freqtrade instal: Conda Environment Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory @@ -308,7 +277,18 @@ Prepare conda-freqtrade environment, using file `environment.yml`, which exist i conda env create -n freqtrade-conda -f environment.yml ``` -#### Enter/exit freqtrade-conda venv: +!!! Note "Creating Conda Environment" + The conda command `create -n` automatically installs all nested dependencies for the selected libraries, general structure of installation command is: + + ```bash + # choose your own packages + conda env create -n [name of the environment] [python version] [packages] + + # point to file with packages + conda env create -n [name of the environment] -f [file] + ``` + +#### Enter/exit freqtrade-conda environment To check available environments, type @@ -322,7 +302,7 @@ Enter installed environment # enter conda environment conda activate freqtrade-conda -# exit conda environment - don`t do it now +# exit conda environment - don't do it now conda deactivate ``` @@ -333,10 +313,8 @@ python3 -m pip install --upgrade pip python3 -m pip install -e . ``` +### Congratulations -### Bravo! - -Click: [You are ready](#you-are-ready), and run the bot ### Important shortcuts @@ -355,74 +333,30 @@ conda activate freqtrade-conda conda deactivate ``` -### Notes - -#### Set of Notes 1 - Conda Settings - -After opening terminal, you already will be in default `base` conda environment. -If you want, you can prevent the (base) conda environment from being activated automatically. - -```bash -conda config --set auto_activate_base false -``` - -Channel `conda-forge` is supposingly best source of the conda updates. Switch to it - -```bash -# adding forge -conda config --env --add channels conda-forge - -# make it strict -conda config --env --set channel_priority strict - -# check status of your conda -conda info -conda config --show channels -conda config --show channel_priority -``` - -Further read on the topic: - -https://towardsdatascience.com/a-guide-to-conda-environments-bc6180fc533?gi=1db972389cd1 - -#### Set of Notes 2 - Conda Environment - -!!! Note "Creating Conda Environment" - The conda command `create -n` automatically installs all nested dependencies for the selected libraries, general structure of installation command is: - -```bash -# choose your own packages -conda env create -n [name of the environment] [python version] [packages] - -# point to file with packages -conda env create -n [name of the environment] -f [file] -``` +### Further info on anaconda !!! Info "New heavy packages" - It may happen that creating a new Conda environment, populated with selected packages at the moment of creation, takes less time than installing a large, heavy dependent, GUI package, into previously set environment. Great example: Spyder + It may happen that creating a new Conda environment, populated with selected packages at the moment of creation takes less time than installing a large, heavy library or application, into previously set environment. !!! Warning "pip install within conda" - Please read the section [Market order pricing](#market-order-pricing) section when using market orders. - The documentation of conda says that pip should NOT be used within conda, because internal problems can occur. - However, they are rare. https://www.anaconda.com/blog/using-pip-in-a-conda-environment + However, they are rare. [Anaconda Blogpost](https://www.anaconda.com/blog/using-pip-in-a-conda-environment) - Nevertherless, that is why, the `conda-forge` channel is preferred: + Nevertheless, that is why, the `conda-forge` channel is preferred: * more libraries are available (less need for `pip`) * `conda-forge` works better with `pip` * the libraries are newer - Happy trading! ----- ## You are ready -You had installed the bot +You've made it this far, so you have successfully installed freqtrade. -#### Initialize the configuration +### Initialize the configuration ```bash # Step 1 - Initialize user folder @@ -432,19 +366,24 @@ freqtrade create-userdir --userdir user_data freqtrade new-config --config config.json ``` -You are ready to run, read [Bot Configuration](configuration.md), remember to run program as `dry_run: True` and verify that everything is working. +You are ready to run, read [Bot Configuration](configuration.md), remember to start with `dry_run: True` and verify that everything is working. +To learn how to setup your configuration, please refer to the [Bot Configuration](configuration.md) documentation page. -> *To edit the config please refer to [Bot Configuration](configuration.md).* - - -#### Run the Bot +### Start the Bot ```bash -freqtrade trade -c config.json +freqtrade trade --config config.json --strategy SampleStrategy ``` -#### Problem? +!!! Warning + You should read through the rest of the documentation, backtest the strategy you're going to use, and use dry-run before enabling trading with real money. + +----- + +## Troubleshooting + +### Common problem: "command not found" If you used (1)`Script` or (2)`Manual` installation, you need to run the bot in virtual environment. If you get error as below, make sure venv is active. @@ -456,9 +395,6 @@ bash: freqtrade: command not found source ./.env/bin/activate ``` ------ -## Troubleshooting - ### MacOS installation error Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. @@ -484,8 +420,3 @@ brew install hdf5 c-blosc ``` After this, please run the installation (script) again. - ------ - -Now you have an environment ready, the next step is -[Bot Configuration](configuration.md). From 5d18289821eb05229d19f390dc5f81b7ce4ec251 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jan 2021 07:17:25 +0100 Subject: [PATCH 264/563] Fix name in issue template --- .github/ISSUE_TEMPLATE/question.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index f87b78f29..fd3d7df93 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,5 +1,5 @@ --- -name: BQuestion +name: Question about: Ask a question you could not find an answer in the docs title: '' labels: "Question" From 406682c3bb93dd527528077e6d278d907c8ce9bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jan 2021 10:20:40 +0100 Subject: [PATCH 265/563] Fix random api failure in slow cases --- tests/rpc/test_rpc_apiserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f5b9a58f3..b050e5694 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -588,7 +588,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc) - assert rc.json() == {'avg_duration': '0:00:00', + assert rc.json() == {'avg_duration': ANY, 'best_pair': 'ETH/BTC', 'best_rate': 6.2, 'first_trade_date': 'just now', From 6b63129eb011f7ceb171c21fbd9c58e82d8e2bf5 Mon Sep 17 00:00:00 2001 From: andre-ac <47426123+andre-ac@users.noreply.github.com> Date: Sat, 30 Jan 2021 15:36:59 +0000 Subject: [PATCH 266/563] Fixed virtualenv link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db648198f..51a3409ea 100644 --- a/README.md +++ b/README.md @@ -191,5 +191,5 @@ To run this bot we recommend you a cloud instance with a minimum of: - [pip](https://pip.pypa.io/en/stable/installing/) - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) -- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) +- [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) - [Docker](https://www.docker.com/products/docker) (Recommended) From 30e5c01cb113baf6cb0f60b4987bd079cf8e1e42 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jan 2021 19:59:14 +0100 Subject: [PATCH 267/563] Improve formatting of custom_stoploss docs --- docs/strategy-advanced.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 25d217d34..ca20d3588 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -33,8 +33,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True - def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, **kwargs) -> float: + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -83,8 +83,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True - def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. if current_time - timedelta(minutes=120) > trade.open_date: @@ -109,8 +109,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True - def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -135,8 +135,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True - def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: if current_profit < 0.04: return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss @@ -167,8 +167,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True - def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price if current_profit > 0.40: From 16dad8b6d4d4d40b8b1019f87612b8a27d37c243 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jan 2021 20:11:18 +0100 Subject: [PATCH 268/563] Allow custom_stoploss to cooperate with stoploss on exchange --- docs/configuration.md | 1 + freqtrade/freqtradebot.py | 3 +- freqtrade/resolvers/strategy_resolver.py | 1 + freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/rpc.py | 1 + tests/test_freqtradebot.py | 103 +++++++++++++++++++++++ 6 files changed, 109 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 660dd6171..509214b9c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -136,6 +136,7 @@ Values set in the configuration file always overwrite values set in the strategy * `trailing_stop_positive` * `trailing_stop_positive_offset` * `trailing_only_offset_is_reached` +* `use_custom_stoploss` * `process_only_new_candles` * `order_types` * `order_time_in_force` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f45d4cacc..d7116834a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -998,7 +998,8 @@ class FreqtradeBot(LoggingMixin): logger.warning('Stoploss order was cancelled, but unable to recreate one.') # Finally we check if stoploss on exchange should be moved up because of trailing. - if stoploss_order and self.config.get('trailing_stop', False): + if stoploss_order and (self.config.get('trailing_stop', False) + or self.config.get('use_custom_stoploss', False)): # if trailing stoploss is enabled we check if stoploss value has changed # in which case we cancel stoploss order and put another one with new # value immediately diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 26a316873..b1b66e3ae 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -68,6 +68,7 @@ class StrategyResolver(IResolver): ("trailing_stop_positive", None, None), ("trailing_stop_positive_offset", 0.0, None), ("trailing_only_offset_is_reached", None, None), + ("use_custom_stoploss", None, None), ("process_only_new_candles", None, None), ("order_types", None, None), ("order_time_in_force", None, None), diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index c9e8aaceb..4faefb5fc 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -123,6 +123,7 @@ class ShowConfig(BaseModel): trailing_stop_positive: Optional[float] trailing_stop_positive_offset: Optional[float] trailing_only_offset_is_reached: Optional[bool] + use_custom_stoploss: Optional[bool] timeframe: str timeframe_ms: int timeframe_min: int diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 92cd6caf9..c5166c35e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -129,6 +129,7 @@ class RPC: 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), + 'use_custom_stoploss': config.get('use_custom_stoploss'), 'bot_name': config.get('bot_name', 'freqtrade'), 'timeframe': config.get('timeframe'), 'timeframe_ms': timeframe_to_msecs(config['timeframe'] diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 06c08075b..2408afc87 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1570,6 +1570,109 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) +@pytest.mark.usefixtures("init_persistence") +def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, + limit_buy_order, limit_sell_order) -> None: + # When trailing stoploss is set + stoploss = MagicMock(return_value={'id': 13434334}) + patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + sell=MagicMock(return_value={'id': limit_sell_order['id']}), + get_fee=fee, + stoploss=stoploss, + stoploss_adjust=MagicMock(return_value=True), + ) + + # enabling TSL + default_conf['use_custom_stoploss'] = True + + # disabling ROI + default_conf['minimal_roi']['0'] = 999999999 + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + # enabling stoploss on exchange + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + # setting stoploss + freqtrade.strategy.custom_stoploss = lambda *args, **kwargs: -0.04 + + # setting stoploss_on_exchange_interval to 60 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 + + patch_get_signal(freqtrade) + + freqtrade.enter_positions() + trade = Trade.query.first() + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = 100 + + stoploss_order_hanging = MagicMock(return_value={ + 'id': 100, + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'info': { + 'stopPrice': '0.000011134' + } + }) + + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + # price jumped 2x + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ + 'bid': 0.00002344, + 'ask': 0.00002346, + 'last': 0.00002344 + })) + + cancel_order_mock = MagicMock() + stoploss_order_mock = MagicMock(return_value={'id': 13434334}) + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + + # stoploss should not be updated as the interval is 60 seconds + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + cancel_order_mock.assert_not_called() + stoploss_order_mock.assert_not_called() + + assert freqtrade.handle_trade(trade) is False + assert trade.stop_loss == 0.00002346 * 0.96 + assert trade.stop_loss_pct == -0.04 + + # setting stoploss_on_exchange_interval to 0 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') + stoploss_order_mock.assert_called_once_with(amount=85.32423208, + pair='ETH/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=0.00002346 * 0.96) + + # price fell below stoploss, so dry-run sells trade. + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ + 'bid': 0.00002144, + 'ask': 0.00002146, + 'last': 0.00002144 + })) + assert freqtrade.handle_trade(trade) is True + + def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: From 4cc93151c5266f77298989262def6778bb85a63f Mon Sep 17 00:00:00 2001 From: Pan Long Date: Sun, 31 Jan 2021 12:14:09 +0800 Subject: [PATCH 269/563] Fix a bug when compare sell_profit_offset It should be comparing the ratio instead of absolut profit. Also updated the comment. --- freqtrade/strategy/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c58d9aa5d..77d45b445 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -530,8 +530,8 @@ class IStrategy(ABC): current_time=date)) if (ask_strategy.get('sell_profit_only', False) - and trade.calc_profit(rate=rate) <= ask_strategy.get('sell_profit_offset', 0)): - # Negative profits and sell_profit_only - ignore sell signal + and trade.calc_profit_ratio(rate=rate) <= ask_strategy.get('sell_profit_offset', 0)): + # sell_profit_only and profit doesn't reach the offset - ignore sell signal sell_signal = False else: sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True) From bc586fe73b40548e92676d1dd251d1ec383d08e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 10:29:43 +0100 Subject: [PATCH 270/563] Try fix CI --- build_helpers/install_windows.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 5747db335..2025a1a44 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -2,7 +2,7 @@ # Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib # Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/xxxxxxx/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -python -m pip install --upgrade pip +python -m pip install --upgrade pip==21.0.0 $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" From 92721db583f690d65f300d5d61aa5e11f1727056 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 10:39:37 +0100 Subject: [PATCH 271/563] Version bump numpy to 1.20.0 --- build_helpers/install_windows.ps1 | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 2025a1a44..5747db335 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -2,7 +2,7 @@ # Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib # Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/xxxxxxx/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -python -m pip install --upgrade pip==21.0.0 +python -m pip install --upgrade pip $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" diff --git a/requirements.txt b/requirements.txt index cc8861d82..c6cb3e445 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.19.5 +numpy==1.20.0 pandas==1.2.1 ccxt==1.40.99 From 5724371a4ff9087a9adb95895e1f829102ceb033 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 11:21:23 +0100 Subject: [PATCH 272/563] Fix types for numpy 1.20.0 upgrade --- freqtrade/data/btanalysis.py | 2 +- freqtrade/misc.py | 15 --------------- freqtrade/rpc/rpc.py | 4 ++-- tests/test_misc.py | 24 +++--------------------- 4 files changed, 6 insertions(+), 39 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index b7c8529ac..828fb78f3 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -312,7 +312,7 @@ def calculate_market_change(data: Dict[str, pd.DataFrame], column: str = "close" end = df[column].dropna().iloc[-1] tmp_means.append((end - start) / start) - return np.mean(tmp_means) + return float(np.mean(tmp_means)) def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame], diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 359d0d0e4..22e14b564 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -9,7 +9,6 @@ from pathlib import Path from typing import Any from typing.io import IO -import numpy as np import rapidjson @@ -28,20 +27,6 @@ def shorten_date(_date: str) -> str: return new_date -############################################ -# Used by scripts # -# Matplotlib doesn't support ::datetime64, # -# so we need to convert it into ::datetime # -############################################ -def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray: - """ - Convert an pandas-array of timestamps into - An numpy-array of datetimes - :return: numpy-array of datetime - """ - return dates.dt.to_pydatetime() - - def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None: """ Dump JSON data into a file diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c5166c35e..491d1cde6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -378,7 +378,7 @@ class RPC: # Prepare data to display profit_closed_coin_sum = round(sum(profit_closed_coin), 8) - profit_closed_ratio_mean = mean(profit_closed_ratio) if profit_closed_ratio else 0.0 + profit_closed_ratio_mean = float(mean(profit_closed_ratio) if profit_closed_ratio else 0.0) profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0 profit_closed_fiat = self._fiat_converter.convert_amount( @@ -388,7 +388,7 @@ class RPC: ) if self._fiat_converter else 0 profit_all_coin_sum = round(sum(profit_all_coin), 8) - profit_all_ratio_mean = mean(profit_all_ratio) if profit_all_ratio else 0.0 + profit_all_ratio_mean = float(mean(profit_all_ratio) if profit_all_ratio else 0.0) profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0 profit_all_fiat = self._fiat_converter.convert_amount( profit_all_coin_sum, diff --git a/tests/test_misc.py b/tests/test_misc.py index 6dcd9fbe5..429da135a 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,11 +6,9 @@ from unittest.mock import MagicMock import pytest -from freqtrade.data.converter import ohlcv_to_dataframe -from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, file_load_json, - format_ms_time, pair_to_filename, plural, render_template, - render_template_with_fallback, safe_value_fallback, - safe_value_fallback2, shorten_date) +from freqtrade.misc import (file_dump_json, file_load_json, format_ms_time, pair_to_filename, + plural, render_template, render_template_with_fallback, + safe_value_fallback, safe_value_fallback2, shorten_date) def test_shorten_date() -> None: @@ -19,22 +17,6 @@ def test_shorten_date() -> None: assert shorten_date(str_data) == str_shorten_data -def test_datesarray_to_datetimearray(ohlcv_history_list): - dataframes = ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC", - fill_missing=True) - dates = datesarray_to_datetimearray(dataframes['date']) - - assert isinstance(dates[0], datetime.datetime) - assert dates[0].year == 2017 - assert dates[0].month == 11 - assert dates[0].day == 26 - assert dates[0].hour == 8 - assert dates[0].minute == 50 - - date_len = len(dates) - assert date_len == 2 - - def test_file_dump_json(mocker) -> None: file_open = mocker.patch('freqtrade.misc.open', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock()) From 79087ba1662e41010b3ccb6554613b5d93dd26b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:50:39 +0100 Subject: [PATCH 273/563] Fix intermitted test failure --- tests/commands/test_commands.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 2284209a0..f8ecc8218 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -822,6 +822,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", + "--no-color", ] pargs = get_args(args) pargs['config'] = None @@ -835,6 +836,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): "hyperopt-list", "--best", "--no-details", + "--no-color", ] pargs = get_args(args) pargs['config'] = None @@ -849,6 +851,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): "hyperopt-list", "--profitable", "--no-details", + "--no-color", ] pargs = get_args(args) pargs['config'] = None @@ -862,6 +865,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--profitable", + "--no-color", ] pargs = get_args(args) pargs['config'] = None @@ -891,6 +895,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): "hyperopt-list", "--profitable", "--no-details", + "--no-color", "--max-trades", "20", ] pargs = get_args(args) @@ -906,6 +911,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): "hyperopt-list", "--profitable", "--no-details", + "--no-color", "--min-avg-profit", "0.11", ] pargs = get_args(args) @@ -920,6 +926,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", + "--no-color", "--max-avg-profit", "0.10", ] pargs = get_args(args) @@ -934,6 +941,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", + "--no-color", "--min-total-profit", "0.4", ] pargs = get_args(args) @@ -948,6 +956,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", + "--no-color", "--max-total-profit", "0.4", ] pargs = get_args(args) @@ -962,6 +971,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", + "--no-color", "--min-objective", "0.1", ] pargs = get_args(args) @@ -991,6 +1001,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): "hyperopt-list", "--profitable", "--no-details", + "--no-color", "--min-avg-time", "2000", ] pargs = get_args(args) @@ -1005,6 +1016,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", + "--no-color", "--max-avg-time", "1500", ] pargs = get_args(args) @@ -1019,6 +1031,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", + "--no-color", "--export-csv", "test_file.csv", ] pargs = get_args(args) From 27970b424d96a9e0369c47d0586c78840cbcdd83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 10:31:05 +0100 Subject: [PATCH 274/563] Add webUI serving to api-server --- .gitignore | 1 + freqtrade/rpc/api_server/ui/.gitkeep | 0 freqtrade/rpc/api_server/web_ui.py | 28 +++++++++++++++++++++++++++ freqtrade/rpc/api_server/webserver.py | 4 ++++ requirements.txt | 1 + tests/rpc/test_rpc_apiserver.py | 2 +- 6 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 freqtrade/rpc/api_server/ui/.gitkeep create mode 100644 freqtrade/rpc/api_server/web_ui.py diff --git a/.gitignore b/.gitignore index f206fce66..4720ff5cb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ user_data/* user_data/notebooks/* freqtrade-plot.html freqtrade-profit-plot.html +freqtrade/rpc/api_server/ui/* # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/freqtrade/rpc/api_server/ui/.gitkeep b/freqtrade/rpc/api_server/ui/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py new file mode 100644 index 000000000..971f9e4de --- /dev/null +++ b/freqtrade/rpc/api_server/web_ui.py @@ -0,0 +1,28 @@ +from pathlib import Path + +from fastapi import APIRouter +from fastapi.exceptions import HTTPException +from starlette.responses import FileResponse + + +router_ui = APIRouter() + + +@router_ui.get('/favicon.ico', include_in_schema=False) +async def favicon(): + return FileResponse(Path(__file__).parent / 'ui/favicon.ico') + + +@router_ui.get('/{rest_of_path:path}', include_in_schema=False) +async def index_html(rest_of_path: str): + """ + Emulate path fallback to index.html. + """ + if rest_of_path.startswith('api') or rest_of_path.startswith('.'): + raise HTTPException(status_code=404, detail="Not Found") + uibase = Path(__file__).parent / 'ui' + if (uibase / rest_of_path).is_file(): + return FileResponse(uibase / rest_of_path) + + # Fall back to index.html, as indicated by vue router docs + return FileResponse(uibase / 'index.html') diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 9c0779274..f3eaa1ebc 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -57,12 +57,16 @@ class ApiServer(RPCHandler): from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login from freqtrade.rpc.api_server.api_v1 import router as api_v1 from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public + from freqtrade.rpc.api_server.web_ui import router_ui + app.include_router(api_v1_public, prefix="/api/v1") app.include_router(api_v1, prefix="/api/v1", dependencies=[Depends(http_basic_or_jwt_token)], ) app.include_router(router_login, prefix="/api/v1", tags=["auth"]) + # UI Router MUST be last! + app.include_router(router_ui, prefix='') app.add_middleware( CORSMiddleware, diff --git a/requirements.txt b/requirements.txt index c6cb3e445..5993fb3f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,7 @@ sdnotify==0.3.2 fastapi==0.63.0 uvicorn==0.13.3 pyjwt==2.0.1 +aiofiles==0.6.0 # Support for colorized terminal output colorama==0.4.4 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b050e5694..919481598 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -83,7 +83,7 @@ def assert_response(response, expected_code=200, needs_cors=True): def test_api_not_found(botclient): ftbot, client = botclient - rc = client_post(client, f"{BASE_URI}/invalid_url") + rc = client_get(client, f"{BASE_URI}/invalid_url") assert_response(rc, 404) assert rc.json() == {"detail": "Not Found"} From a47616eed4648ad4b7331251c79b85354bba62d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 13:55:16 +0100 Subject: [PATCH 275/563] Add UI installation subcommand --- freqtrade/commands/__init__.py | 4 +- freqtrade/commands/arguments.py | 12 +++++- freqtrade/commands/deploy_commands.py | 54 +++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 21c5d6812..784b99bed 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -10,8 +10,8 @@ from freqtrade.commands.arguments import Arguments from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.data_commands import (start_convert_data, start_download_data, start_list_data) -from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt, - start_new_strategy) +from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui, + start_new_hyperopt, start_new_strategy) from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index a6c8a245f..b39c75640 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -167,8 +167,8 @@ class Arguments: from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir, start_download_data, start_edge, start_hyperopt, - start_hyperopt_list, start_hyperopt_show, start_list_data, - start_list_exchanges, start_list_hyperopts, + start_hyperopt_list, start_hyperopt_show, start_install_ui, + start_list_data, start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, start_list_timeframes, start_new_config, start_new_hyperopt, start_new_strategy, start_plot_dataframe, start_plot_profit, @@ -355,6 +355,14 @@ class Arguments: test_pairlist_cmd.set_defaults(func=start_test_pairlist) self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd) + # Add install-ui subcommand + install_ui_cmd = subparsers.add_parser( + 'install-ui', + help='Install FreqUI', + ) + install_ui_cmd.set_defaults(func=start_install_ui) + self._build_args(optionlist=[], parser=install_ui_cmd) + # Add Plotting subcommand plot_dataframe_cmd = subparsers.add_parser( 'plot-dataframe', diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index a0105e140..b43ad5970 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -137,3 +137,57 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) else: raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") + + +def clean_ui_subdir(directory: Path): + print(directory) + if directory.is_dir(): + logger.info("Removing UI directory content") + + for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root + if p.name == '.gitkeep': + continue + if p.is_file(): + p.unlink() + elif p.is_dir(): + p.rmdir() + + +def download_and_install_ui(dest_folder: Path): + import requests + from io import BytesIO + from zipfile import ZipFile + + base_url = 'https://api.github.com/repos/freqtrade/frequi/' + # Get base UI Repo path + + resp = requests.get(f"{base_url}releases") + resp.raise_for_status() + r = resp.json() + + assets = r[0]['assets_url'] + resp = requests.get(assets) + r = resp.json() + + dl_url = r[0]['browser_download_url'] + logger.info(f"Downloading {dl_url}") + resp = requests.get(dl_url).content + with ZipFile(BytesIO(resp)) as zf: + for fn in zf.filelist: + with zf.open(fn) as x: + destfile = dest_folder / fn.filename + print(destfile) + if fn.is_dir(): + destfile.mkdir(exist_ok=True) + else: + destfile.write_bytes(x.read()) + + +def start_install_ui(args: Dict[str, Any]) -> None: + + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + # First make sure the assets are removed. + clean_ui_subdir(dest_folder) + + # Download a new version + download_and_install_ui(dest_folder) From 87ed2d750205d0e1b10a7c185f853cbb13a51d9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 14:06:06 +0100 Subject: [PATCH 276/563] Write some tests for UI Downloading --- freqtrade/commands/deploy_commands.py | 46 +++++++++---------- tests/commands/test_commands.py | 65 ++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index b43ad5970..e951c962f 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -3,6 +3,8 @@ import sys from pathlib import Path from typing import Any, Dict +import requests + from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES @@ -140,9 +142,8 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: def clean_ui_subdir(directory: Path): - print(directory) if directory.is_dir(): - logger.info("Removing UI directory content") + logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root if p.name == '.gitkeep': @@ -153,11 +154,28 @@ def clean_ui_subdir(directory: Path): p.rmdir() -def download_and_install_ui(dest_folder: Path): - import requests +def download_and_install_ui(dest_folder: Path, dl_url: str): from io import BytesIO from zipfile import ZipFile + logger.info(f"Downloading {dl_url}") + resp = requests.get(dl_url).content + with ZipFile(BytesIO(resp)) as zf: + for fn in zf.filelist: + with zf.open(fn) as x: + destfile = dest_folder / fn.filename + if fn.is_dir(): + destfile.mkdir(exist_ok=True) + else: + destfile.write_bytes(x.read()) + + +def start_install_ui(args: Dict[str, Any]) -> None: + + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + # First make sure the assets are removed. + clean_ui_subdir(dest_folder) + base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path @@ -170,24 +188,6 @@ def download_and_install_ui(dest_folder: Path): r = resp.json() dl_url = r[0]['browser_download_url'] - logger.info(f"Downloading {dl_url}") - resp = requests.get(dl_url).content - with ZipFile(BytesIO(resp)) as zf: - for fn in zf.filelist: - with zf.open(fn) as x: - destfile = dest_folder / fn.filename - print(destfile) - if fn.is_dir(): - destfile.mkdir(exist_ok=True) - else: - destfile.write_bytes(x.read()) - - -def start_install_ui(args: Dict[str, Any]) -> None: - - dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' - # First make sure the assets are removed. - clean_ui_subdir(dest_folder) # Download a new version - download_and_install_ui(dest_folder) + download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index f8ecc8218..12d18b3a7 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1,16 +1,19 @@ +from io import BytesIO import re from pathlib import Path from unittest.mock import MagicMock, PropertyMock +from zipfile import ZipFile import arrow import pytest from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, - start_hyperopt_list, start_hyperopt_show, start_list_data, - start_list_exchanges, start_list_hyperopts, start_list_markets, - start_list_strategies, start_list_timeframes, start_new_hyperopt, - start_new_strategy, start_show_trades, start_test_pairlist, - start_trading) + start_hyperopt_list, start_hyperopt_show, start_install_ui, + start_list_data, start_list_exchanges, start_list_hyperopts, + start_list_markets, start_list_strategies, start_list_timeframes, + start_new_hyperopt, start_new_strategy, start_show_trades, + start_test_pairlist, start_trading) +from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -546,7 +549,7 @@ def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog): start_new_hyperopt(get_args(args)) -def test_start_new_hyperopt_no_arg(mocker, caplog): +def test_start_new_hyperopt_no_arg(mocker): args = [ "new-hyperopt", ] @@ -555,6 +558,56 @@ def test_start_new_hyperopt_no_arg(mocker, caplog): start_new_hyperopt(get_args(args)) +def test_start_install_ui(mocker): + clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') + download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') + args = [ + "install-ui", + ] + start_install_ui(args) + assert clean_mock.call_count == 1 + + assert download_mock.call_count == 1 + + +def test_clean_ui_subdir(mocker, tmpdir, caplog): + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", + side_effect=[True, True]) + mocker.patch("freqtrade.commands.deploy_commands.Path.is_file", + side_effect=[False, True]) + rd_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.rmdir") + ul_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.unlink") + + mocker.patch("freqtrade.commands.deploy_commands.Path.glob", + return_value=[Path('test1'), Path('test2'), Path('.gitkeep')]) + folder = Path(tmpdir) / "uitests" + clean_ui_subdir(folder) + assert log_has("Removing UI directory content.", caplog) + assert rd_mock.call_count == 1 + assert ul_mock.call_count == 1 + + +def test_download_and_install_ui(mocker, tmpdir, caplog): + # Should be something "zip-like" + requests_mock = MagicMock() + file_like_object = BytesIO() + with ZipFile(file_like_object, mode='w') as zipfile: + for file in ('test1.txt', 'hello/', 'test2.txt'): + zipfile.writestr(file, file) + file_like_object.seek(0) + requests_mock.content = file_like_object.read() + mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=requests_mock) + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", + side_effect=[True, False]) + mkdir_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.mkdir") + wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") + folder = Path(tmpdir) / "uitests_dl" + download_and_install_ui(folder, 'http://whatever.xxx') + + assert mkdir_mock.call_count == 1 + assert wb_mock.call_count == 2 + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From a1a35115ad7a1c5610920c628ff82fe71b842c67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 14:46:59 +0100 Subject: [PATCH 277/563] Extract get_ui_download_url --- freqtrade/commands/deploy_commands.py | 17 +++++++++++------ tests/commands/test_commands.py | 10 +++++++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index e951c962f..174f1bb59 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -170,12 +170,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str): destfile.write_bytes(x.read()) -def start_install_ui(args: Dict[str, Any]) -> None: - - dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' - # First make sure the assets are removed. - clean_ui_subdir(dest_folder) - +def get_ui_download_url() -> str: base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path @@ -188,6 +183,16 @@ def start_install_ui(args: Dict[str, Any]) -> None: r = resp.json() dl_url = r[0]['browser_download_url'] + return dl_url + + +def start_install_ui(args: Dict[str, Any]) -> None: + + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + # First make sure the assets are removed. + clean_ui_subdir(dest_folder) + + dl_url = get_ui_download_url() # Download a new version download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 12d18b3a7..a39044c18 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -560,13 +560,14 @@ def test_start_new_hyperopt_no_arg(mocker): def test_start_install_ui(mocker): clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') + get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url') download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') args = [ "install-ui", ] start_install_ui(args) assert clean_mock.call_count == 1 - + assert get_url_mock.call_count == 1 assert download_mock.call_count == 1 @@ -588,7 +589,7 @@ def test_clean_ui_subdir(mocker, tmpdir, caplog): def test_download_and_install_ui(mocker, tmpdir, caplog): - # Should be something "zip-like" + # Create zipfile requests_mock = MagicMock() file_like_object = BytesIO() with ZipFile(file_like_object, mode='w') as zipfile: @@ -596,13 +597,16 @@ def test_download_and_install_ui(mocker, tmpdir, caplog): zipfile.writestr(file, file) file_like_object.seek(0) requests_mock.content = file_like_object.read() + mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=requests_mock) + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", side_effect=[True, False]) mkdir_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.mkdir") wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") + folder = Path(tmpdir) / "uitests_dl" - download_and_install_ui(folder, 'http://whatever.xxx') + download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip') assert mkdir_mock.call_count == 1 assert wb_mock.call_count == 2 From ddc99553bd422c250f1d9beba59fccd90211b999 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 14:52:51 +0100 Subject: [PATCH 278/563] Add test case for get_ui_download_url --- tests/commands/test_commands.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index a39044c18..ba45f1618 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -13,7 +13,7 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_list_markets, start_list_strategies, start_list_timeframes, start_new_hyperopt, start_new_strategy, start_show_trades, start_test_pairlist, start_trading) -from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui +from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui, get_ui_download_url from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -588,7 +588,7 @@ def test_clean_ui_subdir(mocker, tmpdir, caplog): assert ul_mock.call_count == 1 -def test_download_and_install_ui(mocker, tmpdir, caplog): +def test_download_and_install_ui(mocker, tmpdir): # Create zipfile requests_mock = MagicMock() file_like_object = BytesIO() @@ -612,6 +612,18 @@ def test_download_and_install_ui(mocker, tmpdir, caplog): assert wb_mock.call_count == 2 +def test_get_ui_download_url(mocker): + response = MagicMock() + response.json = MagicMock( + side_effect=[[{'assets_url': 'http://whatever.json'}], + [{'browser_download_url': 'http://download.zip'}]]) + get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", + return_value=response) + x = get_ui_download_url() + assert get_mock.call_count == 2 + assert x == 'http://download.zip' + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From f05f2c45e89dc9dd394bd1165ce3e7451562f3a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jan 2021 10:15:27 +0100 Subject: [PATCH 279/563] Allow deleting of UI only --- freqtrade/commands/arguments.py | 4 +++- freqtrade/commands/cli_options.py | 6 ++++++ freqtrade/commands/deploy_commands.py | 10 ++++++---- tests/commands/test_commands.py | 15 ++++++++++++++- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index b39c75640..c64c11a18 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -70,6 +70,8 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "timeframe"] +ARGS_INSTALL_UI = ["erase_ui_only"] + ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"] ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", @@ -361,7 +363,7 @@ class Arguments: help='Install FreqUI', ) install_ui_cmd.set_defaults(func=start_install_ui) - self._build_args(optionlist=[], parser=install_ui_cmd) + self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd) # Add Plotting subcommand plot_dataframe_cmd = subparsers.add_parser( diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 668b4abf5..7dc85377d 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -387,6 +387,12 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), + "erase_ui_only": Arg( + '--erase', + help="Clean UI folder, don't download new version.", + action='store_true', + default=False, + ), # Templating options "template": Arg( '--template', diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 174f1bb59..b9bda9c63 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -191,8 +191,10 @@ def start_install_ui(args: Dict[str, Any]) -> None: dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' # First make sure the assets are removed. clean_ui_subdir(dest_folder) + if args.get('erase_ui_only'): + logger.info("Erased UI directory content. Not downloading new version.") + else: + dl_url = get_ui_download_url() - dl_url = get_ui_download_url() - - # Download a new version - download_and_install_ui(dest_folder, dl_url) + # Download a new version + download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index ba45f1618..fdc2b204b 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -565,11 +565,24 @@ def test_start_install_ui(mocker): args = [ "install-ui", ] - start_install_ui(args) + start_install_ui(get_args(args)) assert clean_mock.call_count == 1 assert get_url_mock.call_count == 1 assert download_mock.call_count == 1 + clean_mock.reset_mock() + get_url_mock.reset_mock() + download_mock.reset_mock() + + args = [ + "install-ui", + "--erase", + ] + start_install_ui(get_args(args)) + assert clean_mock.call_count == 1 + assert get_url_mock.call_count == 0 + assert download_mock.call_count == 0 + def test_clean_ui_subdir(mocker, tmpdir, caplog): mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", From e928d2991ddb4ff67aa2d52fda5caf78052e14b9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jan 2021 10:27:15 +0100 Subject: [PATCH 280/563] Add fallback file --- freqtrade/commands/deploy_commands.py | 2 +- .../rpc/api_server/ui/fallback_file.html | 31 +++++++++++++++++++ freqtrade/rpc/api_server/web_ui.py | 5 ++- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 freqtrade/rpc/api_server/ui/fallback_file.html diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index b9bda9c63..8a8d2373e 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -146,7 +146,7 @@ def clean_ui_subdir(directory: Path): logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root - if p.name == '.gitkeep': + if p.name in ('.gitkeep', 'fallback_file.html'): continue if p.is_file(): p.unlink() diff --git a/freqtrade/rpc/api_server/ui/fallback_file.html b/freqtrade/rpc/api_server/ui/fallback_file.html new file mode 100644 index 000000000..7943530af --- /dev/null +++ b/freqtrade/rpc/api_server/ui/fallback_file.html @@ -0,0 +1,31 @@ + + + + + + Freqtrade UI + + + +
+

Freqtrade UI not installed.

+

Please run `freqtrade install-ui` in your terminal to install the UI files and restart your bot.

+

You can then refresh this page and you should see the UI.

+
+ + diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 971f9e4de..6d397da56 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -24,5 +24,8 @@ async def index_html(rest_of_path: str): if (uibase / rest_of_path).is_file(): return FileResponse(uibase / rest_of_path) + index_file = uibase / 'index.html' + if not index_file.is_file(): + return FileResponse(uibase / 'fallback_file.html') # Fall back to index.html, as indicated by vue router docs - return FileResponse(uibase / 'index.html') + return FileResponse(index_file) From 35c2e2556ece3cc93634f4435b6044374794850d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jan 2021 20:48:52 +0100 Subject: [PATCH 281/563] Document FreqUI usage --- docker-compose.yml | 5 ++++ docs/rest-api.md | 68 ++++++++++++++++++++++++++-------------------- mkdocs.yml | 2 +- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7094500b4..06cce4e8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,11 @@ services: container_name: freqtrade volumes: - "./user_data:/freqtrade/user_data" + # Expose api on port 8080 (localhost only) + # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation + # before enabling this. + #ports: + # - "127.0.0.1:8080:8080" # Default command used when running `docker compose up` command: > trade diff --git a/docs/rest-api.md b/docs/rest-api.md index 2c7142c61..e2b94f080 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -1,4 +1,19 @@ -# REST API Usage +# REST API & FreqUI + +## FreqUI + +Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade UI. + +By default, the UI is not included in the installation (except for docker images), and must be installed explicitly with `freqtrade install-ui`. +This same command can also be used to update freqUI, should there be a new release. + +Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`). + +!!! info "Alpha release" + FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose). + +!!! Note "developers" + Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI. ## Configuration @@ -23,9 +38,6 @@ Sample configuration: !!! Danger "Security warning" By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. -!!! Danger "Password selection" - Please make sure to select a very strong, unique password to protect your bot from unauthorized access. - You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly. This should return the response: @@ -35,16 +47,22 @@ This should return the response: All other endpoints return sensitive info and require authentication and are therefore not available through a web browser. -To generate a secure password, either use a password manager, or use the below code snipped. +### Security + +To generate a secure password, best use a password manager, or use the below code. ``` python import secrets secrets.token_hex() ``` -!!! Hint +!!! Hint "JWT token" Use the same method to also generate a JWT secret key (`jwt_secret_key`). +!!! Danger "Password selection" + Please make sure to select a very strong, unique password to protect your bot from unauthorized access. + Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!). + ### Configuration with docker If you run your bot using docker, you'll need to have the bot listen to incoming connections. The security is then handled by docker. @@ -57,28 +75,20 @@ If you run your bot using docker, you'll need to have the bot listen to incoming }, ``` -Add the following to your docker command: +Uncomment the following from your docker-compose file: -``` bash - -p 127.0.0.1:8080:8080 -``` - -A complete sample-command may then look as follows: - -```bash -docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -p 127.0.0.1:8080:8080 \ - freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +```yml + ports: + - "127.0.0.1:8080:8080" ``` !!! Danger "Security warning" - By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. + By using `8080:8080` in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot. -## Consuming the API + +## Rest API + +### Consuming the API You can consume the API by using the script `scripts/rest_client.py`. The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system. @@ -89,7 +99,7 @@ python3 scripts/rest_client.py [optional parameters] By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour. -### Minimalistic client config +#### Minimalistic client config ``` json { @@ -105,7 +115,7 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use python3 scripts/rest_client.py --config rest_config.json [optional parameters] ``` -## Available endpoints +### Available endpoints | Command | Description | |----------|-------------| @@ -264,12 +274,12 @@ whitelist ``` -## OpenAPI interface +### OpenAPI interface To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration. This will enable the Swagger UI at the `/docs` endpoint. By default, that's running at http://localhost:8080/docs/ - but it'll depend on your settings. -## Advanced API usage using JWT tokens +### Advanced API usage using JWT tokens !!! Note The below should be done in an application (a Freqtrade REST API client, which fetches info via API), and is not intended to be used on a regular basis. @@ -294,9 +304,9 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques {"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"} ``` -## CORS +### CORS -All web-based frontends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. +All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately. diff --git a/mkdocs.yml b/mkdocs.yml index 4545e8d84..2d37ddba8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,7 @@ nav: - Control the bot: - Telegram: telegram-usage.md - Web Hook: webhook-config.md - - REST API: rest-api.md + - REST API & FreqUI: rest-api.md - Data Downloading: data-download.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md From 1df0aa875193fc64a1c9ec6d8aae9bd4aeda277b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jan 2021 21:08:15 +0100 Subject: [PATCH 282/563] Add ui installation to docker container builds --- Dockerfile | 4 +++- Dockerfile.armhf | 4 +++- docker-compose.yml | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 445f909b0..8d4f0ebe6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,9 @@ COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ RUN pip install -e . --no-cache-dir \ - && mkdir /freqtrade/user_data/ + && mkdir /freqtrade/user_data/ \ + && freqtrade install-ui + ENTRYPOINT ["freqtrade"] # Default to trade mode CMD [ "trade" ] diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 2c7d4538a..ec6aa72b8 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -41,7 +41,9 @@ COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir +RUN pip install -e . --no-cache-dir \ + && freqtrade install-ui + ENTRYPOINT ["freqtrade"] # Default to trade mode CMD [ "trade" ] diff --git a/docker-compose.yml b/docker-compose.yml index 06cce4e8e..1f63059f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,8 +17,8 @@ services: # Expose api on port 8080 (localhost only) # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation # before enabling this. - #ports: - # - "127.0.0.1:8080:8080" + # ports: + # - "127.0.0.1:8080:8080" # Default command used when running `docker compose up` command: > trade From 2af1d2d6390357c267b0ab4a3ccb5e07e7086576 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:38:40 +0100 Subject: [PATCH 283/563] Extract last FreqUI version from api response --- freqtrade/commands/deploy_commands.py | 24 ++++++++++++++++-------- tests/commands/test_commands.py | 25 +++++++++++++++++++++---- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 8a8d2373e..44ddc1fdc 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -1,7 +1,7 @@ import logging import sys from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Tuple import requests @@ -170,7 +170,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str): destfile.write_bytes(x.read()) -def get_ui_download_url() -> str: +def get_ui_download_url() -> Tuple[str, str]: base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path @@ -178,23 +178,31 @@ def get_ui_download_url() -> str: resp.raise_for_status() r = resp.json() - assets = r[0]['assets_url'] - resp = requests.get(assets) - r = resp.json() + latest_version = r[0]['name'] + assets = r[0].get('assets', []) + dl_url = '' + if assets and len(assets) > 0: + dl_url = assets[0]['browser_download_url'] - dl_url = r[0]['browser_download_url'] - return dl_url + # URL not found - try assets url + if not dl_url: + assets = r[0]['assets_url'] + resp = requests.get(assets) + r = resp.json() + dl_url = r[0]['browser_download_url'] + + return dl_url, latest_version def start_install_ui(args: Dict[str, Any]) -> None: dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' # First make sure the assets are removed. + dl_url, latest_version = get_ui_download_url() clean_ui_subdir(dest_folder) if args.get('erase_ui_only'): logger.info("Erased UI directory content. Not downloading new version.") else: - dl_url = get_ui_download_url() # Download a new version download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index fdc2b204b..dcc0cd1d2 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1,5 +1,5 @@ -from io import BytesIO import re +from io import BytesIO from pathlib import Path from unittest.mock import MagicMock, PropertyMock from zipfile import ZipFile @@ -13,7 +13,8 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_list_markets, start_list_strategies, start_list_timeframes, start_new_hyperopt, start_new_strategy, start_show_trades, start_test_pairlist, start_trading) -from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui, get_ui_download_url +from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, + get_ui_download_url) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -628,15 +629,31 @@ def test_download_and_install_ui(mocker, tmpdir): def test_get_ui_download_url(mocker): response = MagicMock() response.json = MagicMock( - side_effect=[[{'assets_url': 'http://whatever.json'}], + side_effect=[[{'assets_url': 'http://whatever.json', 'name': '0.0.1'}], [{'browser_download_url': 'http://download.zip'}]]) get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=response) - x = get_ui_download_url() + x, last_version = get_ui_download_url() assert get_mock.call_count == 2 + assert last_version == '0.0.1' assert x == 'http://download.zip' +def test_get_ui_download_url_direct(mocker): + response = MagicMock() + response.json = MagicMock( + side_effect=[[{ + 'assets_url': 'http://whatever.json', + 'name': '0.0.1', + 'assets': [{'browser_download_url': 'http://download11.zip'}]}]]) + get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", + return_value=response) + x, last_version = get_ui_download_url() + assert get_mock.call_count == 1 + assert last_version == '0.0.1' + assert x == 'http://download11.zip' + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From 944d674eeb6d590685f1970c60210f80b93fc726 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:39:46 +0100 Subject: [PATCH 284/563] Store freqUI version and read it again --- freqtrade/commands/deploy_commands.py | 25 +++++++++++++++++++++---- tests/commands/test_commands.py | 10 ++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 44ddc1fdc..c4e958df6 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -1,7 +1,7 @@ import logging import sys from pathlib import Path -from typing import Any, Dict, Tuple +from typing import Any, Dict, Optional, Tuple import requests @@ -154,7 +154,16 @@ def clean_ui_subdir(directory: Path): p.rmdir() -def download_and_install_ui(dest_folder: Path, dl_url: str): +def read_ui_version(dest_folder: Path) -> Optional[str]: + file = dest_folder / '.uiversion' + if not file.is_file(): + return None + + with file.open('r') as f: + return f.read() + + +def download_and_install_ui(dest_folder: Path, dl_url: str, version: str): from io import BytesIO from zipfile import ZipFile @@ -168,6 +177,8 @@ def download_and_install_ui(dest_folder: Path, dl_url: str): destfile.mkdir(exist_ok=True) else: destfile.write_bytes(x.read()) + with (dest_folder / '.uiversion').open('w') as f: + f.write(version) def get_ui_download_url() -> Tuple[str, str]: @@ -199,10 +210,16 @@ def start_install_ui(args: Dict[str, Any]) -> None: dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' # First make sure the assets are removed. dl_url, latest_version = get_ui_download_url() - clean_ui_subdir(dest_folder) + + curr_version = read_ui_version(dest_folder) + if curr_version == latest_version and not args.get('erase_ui_only'): + logger.info(f"UI already uptodate, FreqUI Version {curr_version}.") + return + if args.get('erase_ui_only'): + clean_ui_subdir(dest_folder) logger.info("Erased UI directory content. Not downloading new version.") else: # Download a new version - download_and_install_ui(dest_folder, dl_url) + download_and_install_ui(dest_folder, dl_url, latest_version) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index dcc0cd1d2..b243df192 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -14,7 +14,7 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_new_hyperopt, start_new_strategy, start_show_trades, start_test_pairlist, start_trading) from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, - get_ui_download_url) + get_ui_download_url, read_ui_version) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -563,6 +563,7 @@ def test_start_install_ui(mocker): clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url') download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') + mocker.patch('freqtrade.commands.deploy_commands.read_ui_version', return_value=None) args = [ "install-ui", ] @@ -616,15 +617,16 @@ def test_download_and_install_ui(mocker, tmpdir): mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", side_effect=[True, False]) - mkdir_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.mkdir") wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") folder = Path(tmpdir) / "uitests_dl" - download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip') + folder.mkdir(exist_ok=True) + download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip', '22') - assert mkdir_mock.call_count == 1 assert wb_mock.call_count == 2 + assert read_ui_version(folder) == '22' + def test_get_ui_download_url(mocker): response = MagicMock() From 7b3d99819f60f03a9464c2f67720ed7c83c17dc2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:43:16 +0100 Subject: [PATCH 285/563] Fix bug with not cleaning UI folder --- freqtrade/commands/deploy_commands.py | 5 ++--- tests/commands/test_commands.py | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index c4e958df6..62da6abf3 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -213,13 +213,12 @@ def start_install_ui(args: Dict[str, Any]) -> None: curr_version = read_ui_version(dest_folder) if curr_version == latest_version and not args.get('erase_ui_only'): - logger.info(f"UI already uptodate, FreqUI Version {curr_version}.") + logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.") return + clean_ui_subdir(dest_folder) if args.get('erase_ui_only'): - clean_ui_subdir(dest_folder) logger.info("Erased UI directory content. Not downloading new version.") else: - # Download a new version download_and_install_ui(dest_folder, dl_url, latest_version) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index b243df192..0d2b9e394 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -561,7 +561,8 @@ def test_start_new_hyperopt_no_arg(mocker): def test_start_install_ui(mocker): clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') - get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url') + get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url', + return_value=('https://example.com/whatever', '0.0.1')) download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') mocker.patch('freqtrade.commands.deploy_commands.read_ui_version', return_value=None) args = [ @@ -582,7 +583,7 @@ def test_start_install_ui(mocker): ] start_install_ui(get_args(args)) assert clean_mock.call_count == 1 - assert get_url_mock.call_count == 0 + assert get_url_mock.call_count == 1 assert download_mock.call_count == 0 From a87a885ccd3221fda93d3f1c3a37c754ba64403e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:54:58 +0100 Subject: [PATCH 286/563] Don't use Path object to return fileresponses --- freqtrade/rpc/api_server/web_ui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 6d397da56..4876c9077 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -22,10 +22,10 @@ async def index_html(rest_of_path: str): raise HTTPException(status_code=404, detail="Not Found") uibase = Path(__file__).parent / 'ui' if (uibase / rest_of_path).is_file(): - return FileResponse(uibase / rest_of_path) + return FileResponse(str(uibase / rest_of_path)) index_file = uibase / 'index.html' if not index_file.is_file(): - return FileResponse(uibase / 'fallback_file.html') + return FileResponse(str(uibase / 'fallback_file.html')) # Fall back to index.html, as indicated by vue router docs - return FileResponse(index_file) + return FileResponse(str(index_file)) From 28be71806f000a0ba5a96307d721065ae42c76fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 15:00:13 +0100 Subject: [PATCH 287/563] Install html file as well --- MANIFEST.in | 2 ++ freqtrade/rpc/api_server/ui/.gitkeep | 0 freqtrade/rpc/api_server/ui/favicon.ico | Bin 0 -> 126794 bytes 3 files changed, 2 insertions(+) delete mode 100644 freqtrade/rpc/api_server/ui/.gitkeep create mode 100644 freqtrade/rpc/api_server/ui/favicon.ico diff --git a/MANIFEST.in b/MANIFEST.in index 2f59bcc7a..adbcd2e30 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,5 @@ include LICENSE include README.md recursive-include freqtrade *.py recursive-include freqtrade/templates/ *.j2 *.ipynb +include freqtrade/rpc/api_server/ui/fallback_file.html +include freqtrade/rpc/api_server/ui/favicon.ico diff --git a/freqtrade/rpc/api_server/ui/.gitkeep b/freqtrade/rpc/api_server/ui/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/freqtrade/rpc/api_server/ui/favicon.ico b/freqtrade/rpc/api_server/ui/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..78c7e43b1e6a5756492a1d53813e1ec47f98efdf GIT binary patch literal 126794 zcmeHQ378ed5q|3m3mz=dctpe%QM{t!eWB}_D8?I2G%6YsyyA_T7;zWzict*lz$=0m zibq7nAR1u>BZwC&i6+swDg;pw5X2Ri?fn1j%+S-XyXSiE?JjSMZ<(2%?&|95dOh9M zRn@s%1^zwuOb-8Vn%iYzMQ;6EF1Ho_9>d?Diq>@8W>6#!eDJ{sJ;#n6d+hDE-#%#Q z(4m+8@|V9HdDKxyjooj*{U+_S(@t-0v&}X$dEI~i{U;uM^wDE}^{Zcvy!P5_2Q&Y; zapMmE=%bG|ED~uM+A`*V5TatrlqtJia>*sv9dpbv4{xx+2D8Bqk@j!njW=#Q@x&AF zyW)x~u9`V>=C)d)}>ExA{@zt5|u~Uw{4C=bd-naE!No%7E=z6Jv0I zW7)p@?)yB7tiWID?|Y$ebn4XUn}ZHIX#AURzPU||=44q{K@LoqFyX-6cH3=gp=7I= zcn&z=fM+L7nzZ*S*bP~eEFAdo!w)w(^2j3}FQ#l212^W*%TGS}Qpg(`gG?y zapFXA|NZxi>#n;_oO4x?!No(ld!g55LZrn_wFrjyX`jdpa1+PxnM>p`OZ7#VcRyvHAZynhGoo{V&G;D7)7-x~P#-VsMeq30Bs6N)$9cq4+86+R2+ z5d#Jc5FE>6(A{#;MHgKQYOrgWK}X{N`mO6e`|R@~=#JFqSht9C&N)Z?^Pm4LyZ|P^ z$lnToGjU`V`)Uue-5ss3%9-AI^X6^Pt5>i0qsnRj{{02!eF-G0_(lHlkAH|=cG)G; z?~U!$TT2y;l2{*o6xhcJJA<=Lb>b6np&w^VyVARNCUO&l1>Qiz=@(KL7mlwc2-& zl_zzLjg3F};SYcKMwpyZH#%a(h=K@NQ&W?(27CD7hsCY8-YRas`DSN54;`-f{qKJ- z{`ki~I(mdhAAMBtTmJ30-=?m2#flXIc8H>)q9RmgF?V|A@;kzg)cf>Z^ismlPg) z=pnJ@nrnvEfBf;sk0yWGXU~-%RVSTv(mk?F=C7{`u!eo-gX^*QKqy z?z)cLzW@IFiA46Tx84%auZGI&;K75hM*SJIF$eCs=bjTmg4I5X`<1=;7n!|FiA5G`?aOTA z71zlxz4TI4F%|#%*T2NByY5<48MM)>dxw3uSYs@8#bNtgBS(%r6?J7$A`Z}(F3heE zeG*wtsZ)xwrbhEhF3Y5gc7{=bwp%tj$v5bgzh9!RD*I~a*J0}q_&G<`2Mr%SJd&=` zR$sB-(VphM`|gV*uGCw=_uDG3urs)`?1iWV3}|R**wZE(TuaTLKR;5x{^?JD5?q7Z z@U@qBoI}%YI|_XK@yBBAwbwSbi~a>Kzx?tZCFriQt^#}Aui511x#ymX(w)xQ#fB&9 zJLM$7b5U9(w4Z0(SgHxrtg2^Tm61@Bsg*6@w)U_39)1R5*|KF$otz)b@3-H6d;eXS+{op^?zu;dF)_5AoC}1l z6<>e-b%yK7pa>kmJZu8c24uo}W*^R)H7m3a;mcC6{U`cSc|e;&d5`{@Yp(HLIam3e zM|09E_ljXh6~M21AU|yN;(PEUXvmjeED**_10S( zaoF(d-q9z}1I`g;eGz?9WRTzKaK%l7u2ir+VSrdJ#`Z#wyX?L9-rE#)XHY;6ARfxC zKxgE86#G8foY-*0eb<(oP$*kF>tdYW;03t5sE1k!)~j+ojU(J;NXIWAYcz85Dp64?OU|u|Q+wcN9N9+J@M0 z*xs$T-g=IGDs`Ol-@EU=>;FcUE?sJT$4o3o+kV*Eh~NG0cLKf(iRqDPrTst{&;~~? zqu#rEKqP07mII9a2pn2|@44rmp?d$YzB1wV(&=|y4B&ngV`6yqB)M`PjF@quqrG4|wHv+%KP#SD> zM$y$FR*-cq-Z~*a1jxhBT=bv-u&b5jIV(c0F$b?r-mt~dT`hWlX---Vi zb43N}Xt`&g)*JPidv9U(+SJwA@L@jF8}Bnn!2#G}{07CfeBOQc-PLZ-dq(_oZCMje zUAio*j1i>E6DMu+%{Lc_;U1tB@!(?n#K?8hFD49Z`Z~sv2fhqaaA5Df_cq3(;2OmS z9Ip^FJ+*8VT2{dhhbGk?<@D1}PfRO)xx!$`kRe9=@B_LxMK@(d!2#&1mV14xUa?1L z?H=0tsb!Kp7fV*T*T(VG1NwC(q0cMB`Hl_fZ|mh%ubXbV>3c}aARz}n{q)myP}Im@ z`jy(?(n~KjmbKwk-xW(%Sr7fPWw7b(Rl3yY-~RTuGOE0L?w45h&sJo1t>9=vc=p+6 zX{(}@H;e;$eW4~GUVyf&38yK&n6ip<0Rm*FvB^62*kc2@bPtoN$7!EBv&UmKNI>V(c$neZ`bj#=VdsjXf=WxWD`EI~iTxJ@wJLHZtw5 zyYBiGt{EiY0Cd!Mp_rDpS6_W~UO&hQ^szRTjhkLfS$+EHr+u<%BOaGFHGf!~Lz^xa zd&cJjUj_*{fEee`p_sP+!uFEj53em}!e>h_maO8eL0?XJ!?~-CjwtU`U09$3Ja8O0 z;W_u*b8o=A3=(hv`qSwsrsXa7)os9dQl_$T(~BXijQzG~(IUSI{C3mFATDicew?G} z3Eczya8`zm8&B-g4Zu!37swpe<*@XG@o5m3A^e{pn8={n3_` zx{1I2?QeO$|BT&cqao_MTW-0<#ukC~s<9rNOY~RNlR*Lw?7Z{NMp^yqU;k?D6P|m5 zDhu)_%c`}~gfAa^?6HFRDDrz^gUOR88+n3#mq`h_DJudF*kqMslMR0U^Pe00B(4k> zQ&wr)Ux>XmuT5cVPWozjdDW|XE*+9Hh|2++tnv&98~oxIzcBU*=!o@Bl~wLRB>mIT zOkb@+$tz>P+JN69Q~MDYl;?bAkbna=S>?RT28=<8Dhu)_%PPn7O*Yvi(O*qXjlg$S zz;8q#=5&C3{`S)BS$S>cyw?UiGtSGaUSaF?7o=s7fCIL*RajgH#x6sZ1^JU@wGehz zq>E=2z%MT#R*YYjmgcb7NE~-fb-)*h`s@r6Z~%KOQ&3FH+c4b>*DI#7anp+-tE7Q@ zz)O}a@#VD;zk_k(#`*YkH#YxB*b@u|T^S_c0CeS#pqN(XIBwd2zJR8(anp+>tE5A= z3#5-gT-wz9jCW`Q`VgpPyl3o1oQ?Yo5^&&(E3T+RF)eRTJMA=EmpH!jg_!@b{$k4N zC6`~kzY-%^YQ*Ys21}QjTJ6|o#-x}u#rt-G~*-qaULsoGnM_#I%80zkyG;J$Rm#orNicLjd-q%Yg3lMb9^z6A9(lO zcMqa}9~ik>8b%<_opDcq=RSwc(`@z{piBE3kkfHSWT-5}%|M+!_mI-YE~GEv^y$+> ztEVrP+$Q<{j5E%-8P8<}U|46y@dE$8LBoCCY_-)^ZzB#lzvYPl{wk-VppQ?z5A!kN zTn#ov#_}pY^_hAe`iL^tg#0IuxAc|b+}L}s+ShvTXjcp0oPwSA#xrNafO8nNP4Y9& z0ox1DWdq<>SF9V}M1zfdx#*&c5}jcS+*R=JH0rKs{}%=vdl;|OMqk`_^8BOJaji)! z&k_oouahtImoc>sHZ+{*mepB;(1#2&@k35WZ1PyXlAQA+*3uB1fwM@)Bj0mv5v9*) zizk;$d*9Zw%GiHlK-+4I{$F7HxE6dU8-S17_pl~6$`Tmi%LvATrHHe)Sr~KB2dmMq z5^;2v%H>V>;CWcL3EGA*?wJWk-1N4TRnBSHo-i0NV1Ti`*w1Z(Z+cI(yKDfu!F|!# z08g;C{Qz_J+bEmrJJ@NyU}FyW-wAQ3A6LuSo~geIo42xG8T09T-}_#GRvTX1yZZWi zUsSkOlE<&Qx;p2NdG6FeB$Fb-Gtei!RG2Q)K^vj`(E1T1o|I7YUhy& zu!%lSeaC0_tSV;7gFpZI&p$_E0AG}6`jE#L+<4=SoP!4Va_60Q4)mDd`bGrabN?Cq?&9T< zuZS1^sfv%!*em)BW6Br0cV1dly*=$;3pv(#^%wI>{S)BSY z5jj%j1?HMJAql(RVi=a>ya$VCy;=IztQ4MFR10?g-gA#zadB^v0q%Ly7GU@CIjvO_2*A9^N0+JeXrHMjXX82r&x{!n6>R=TGS0TVXFRsBI5Tp4ZSmFE4#c#|vr|F&PJ;#w z8iBf&>)MKQW8UT-58kB$#wo?wF%Nd{-hDn0`F#jmg%9O#ajHrzg>l~Ex@cpwTsLAT zzAqO6FS=mw?-B2Bw4CpuhaNgc&7;N#hpu#IjMK(_SC$WpcO~~N#%y8R*HP&y zi#~l1b$9X$#=PP87rMtU zZS}zC>&lpDV`qmhh;fnqw$kntHhJA;MEHiCBIn^fWbRw|G4q^t2XNNXuDJ2>@E-mu zx3|7s`M;|d4CIr`aScM=VVkL>#RXAQOS{%uvq?&O`97Xn?&%%2R+(SZ^O8R|uY0;QKUL&4(Ck z6e{QU>e{9U@L)aryo`tU$X^M)*nd=9@-uDlRKI3hztb+CHlB=)F5@ZnJ(pYe zz4qFU;A!5R2zD+fF)#SfIedV2&hb?|e4aUT=C)WHFH`f&&y0T^7^c(_Ox;9GUe~ptJnL`r|iSI0e-h%$@ys8f#=9z8&6?(H1RjT`OOfl z^;Q6(Pe1zm<;WEz$f!Gq3NfWd=}^IL>LHmn-gD1j{P^+v>&~-;-3aZhw1D5p8F=Bh zQ+Ll;k>I=By`lFU(-;ed-(eljLZ)$zkMrimlqpj}=~6TBtS#zt!|1{O%jB>;d`~|N z>{qXkduQB$tS*tuVV?9B-$en$f;}8_65TnvD{zJmox@fq9{kSc;NrKTXV0D=M8VHI zz)^v}SoREDUtzoqjPuZ;(v~F;zWnmbYJBGwBubMuf$39z%j+g6_MdiU-v z!hGjdIq;r2&M`(N^ZUweuunl-jU4PW2VniJ)lWmF zzJnjj-5|i3i!r~>j~KqP9A`oSv51bsH%}+00yZIje{zo(bCJMzcBh?onr^EAG**oo zHR@!1gT`R(ITv%4CHxkgefHTS(8oP&WxaQpKXUEw_a|icTXlWq<%Rq)Zi1iJbkA>4 z*ctcaFXp_|ky-KaOwuG{mnT49fBm%+zm)q>JU5fN`LH_W_smawTl$I7W{~?`NrAE3 zqkQ+d4}&_K@e8!%Xp6!4D)^nuqTU*I=regP```cmcdlszu*V*IOhQ7SjhILCY&0UW(?@uo8%u(0fky%6M1& z_Vak);c@p10(5%iyldE)>fz$J4*vhcD`&cLoy=J0Jo};$z_~Ez3fw264Aak>XBF~F z{SkDA&YtL;Idck8ofD(O7Druf_jcQD_nIuH9uAVyUS7m^;@*-EIDd0%eA60{V3if`J>?ZKqz3v3G&hLNcY~MTyJG%% zye6xk1ioJadYiob-j#DW{03viS7JN?wCN)chwaL^5A){DD~SA*3JL914m#+dNd2|L z4m-?Pym&Fs2TKI~`}cnm1p{?qkL4a4GOjn&wU~zkYp6Q4EPm6sdGEce+kb(qoSS&> zqh2vDXu{mBCV)tKz+Cz$71x<(p7}>9i}sc_s@!j7tTpJQ-^BjOtvF|VCvE1CMEEcD zLTr&nWDD?F_ske1VX?|eC5K`4z^@|GH=pg~_Z_~18{j?>a8DfLhdS4!-gA2sLd477 zLuXTm`~vgH8_~YN`iFbz*cZrKvtXWktXcrLJxw?>%&#FRHk_XV?`{ z_a7*O`Oq8Q`%+ywUq^oh_9j%ii7hvDR-t`q7V`VY|~Xo8z6j2f+E5$%Yd??K^TGOvhL3H{PkE1#`9Ds+_R==+UE% zYa+z^Jdo#|!G7ahuCsJ_p$mObr2}j3m(WLnG2JVR-}N*#zbE@%o>E;wV<+_YT`E1c zXYS?jyfW?wgvHA$w4Aa&j-&KLw9%-(<67dZv(DmJm>QrHxCTf9^}9NjJlwEZ+5vL9 zP|eS0`o@Ak6?uh_fbZC&Y8lAQ-*tK8q;YQuzcY32B1~s@Vr_q;N=F(W);P49@^bGR zYtuac1eH$pnQ>jX=H{BFn7}cVIs>eS0y3h~qkD$l=}YKA1AE!Xmk3}3)m0aZ!L`E- z;N*G&_C9lwm`5|lhdTn9r3amUfjBtdm9izig$?7w#3%nDZrHmzgsx3QATNHa!*zK} zna1@3Xb#nB$!+xRv1SlE?zp31oPeoQr#gD0G_p&(1K8*|dM)Z{sYmnbigU$S^hzQ9 zrC`rHSgjwndfcc8jF03ce?3UJE)|!rYU3+=2k@7CD~iV@w}aUyA+BS*a>dS(r4^E|zn1 z$QbvU{WT@-^LXCg)2L5^{&O7T(#@SacfFMCaf)*-ueKXw>Ibq;f%-ti&i+Kr$7kwe zRatNlOaEcd?m4wA{12#)DkyNDlXm<-8t7ZtN1ditM7@ce4`Xt3Tdc3ZyUvKkd$Jp^ zDL}VOUkAT$!CGZ3b<}D<#CgU(&mff#uvPH=F}OF2Jzeem6!kv>UTfFd9u2zR0^Y6Q zddSOD$SY|7J@5tk9OKk~)jALdC2#EwTh;+;zG6Oe9=FRbyS$>(yQ)58tX-DW4x0UN z(@i(!*>vKptaJE|>mk@aeTdsY-}4!I*QX$-otV!CnD4)e_kr@R�|f;&=3w!Tcg` z-ip2OQ)Hx=6JDv(Rmw9ys7rzU%{-NcGI(x*o$?5|FQVRKUV4VgAJ`MTfjQ{?s$HO$ zkNS9dj$o>bIwQ{Uy>h(bc}1~(zIApn7+a~-@z;K^)7@XDi}$Xc6Su_$jsd1U6x1Bx z*Q)xAJ?fj|av3`P-Qw^AabG5KjuoXIcl-?S9)5QBMk$*n5BEi&{|)#cvW+;i*0-U7 z&-2<$lmF-knWo+-Wx>0yTo1_gmg#*9zTdx$!ZSG!;ulVc!Y}$Enf8uke7xtk7<*I! zTPo=5zk%&kZ#8c*o`(+~{%hb3$RccbKXYZ&sSKInus zMV?Iwh;r!0zSgu-5r{eA_OSOF!#xhJiJ=FW3)`Z4#%C`!4bb;Bcx{%iuuBLNQ|JAv zAghcYC%0X`=Xw{HbWzMxPa3ex`wLu0gfqxrYyNrZQQLFB0vp1pO|lN2ZUQ z*SGRD>e`CJKsy$>Y3xXr}hu8?-fpB5;5)h2d-XuxlH$0#z7g zERL*FUOCp;@Rjl%_n{_Dnnbc9flXE=x>sPW)`+;cqup~E@G65sbAa(Z7^ew%Bg<1< zS>+lBbwtJ=hnBc`8OtyZM3mJ)+ZewI@yWI*bYGP-KE~aI4}DYI{*EiFlnun>DoHGB zQ(HKm@*JbMkVIBV18fc#6e^BHIm;;9&iydhZE54skk_`qB(h2wS+;DM6H68QD^~or zx|6))IWUY(lmwE>D(Qr-V*zhKD&h9QeD1GR$F-odjNl!C*Mr5-WWG66TD;`z`XEvoeVNQstlQ| ziZI(1nOy?$Xtcj=H@0%J&MNa#CRtVeQZiYMC#$AC5AeHW_Rg!CXlJq-7!IuuwzZW! z2OyG6Rtx`2X-iq9za8UF;LJV2as1LtFBP=CPkQ#CDrm&dSgnjd%yR+Y2P7VU{BePJ z5^B~+&zYYztYXFCo`tpwswAU2B}ZdRf?TMuR}iG9zx*OZ+@HAP!`*8b`7U zpSRSqigRpuHl9_EJQ-$;9*%`kfqwivOVBGHc7^|`h-$k^8Zucey!^$%SSneiudB(2 zBuu8LyVS)Ul4Y1@W;6bJ7%tuWfddE1sEqfStj3Y7!sjiOtRh})B02J8i!s$~z_S9p zyh*NzIc-DlynFW9XN`4aYpcS`riwADtkS+-Cj*=phQY@lf9%8&(v^>zM%_B?33c$! zJMW0Fn0{gP;QV^QbA@ccxP>M>h)WYFn&|<+sRBb(Sv7rMd0tc)AZD$xkI@|GooS!P zd(SK9x;o%lgmOOJy(zwroR?#>4G@3bSPyJ}14T1g1!oHkCRtVIaFi2No%e^BUrsE& zKu1XroJTS~s;(^4=r2tj63=g^uF59Yw9zI$8I~+rA{a~4grD{d94}dpd!IN*+L)Jg z+JNgkQ$4Dl&7|jm&+N;SgRE+5nAqiO=ixippIne95T z#RW!e-aru>e#EE=;ZW8WCjU1 zfOAR5pqQ4gyY9NH;F^Q_bn5F(`KZq{0d@VRayTnjTa4$enaWIY0ELz@B7LL(Z_QJF*pFd{NBiJ z#cZYQo64y2ZCjh;te8A`V}gw)*z;=3&?cO*+D&vtOmD%tn|mT} z1_?QUGcjhOXySh0H`@d@oh0pIbzmFA5yLr9gueYI+D*QHHvIGE%?sf79{3wE)e|-S z;DZkykGvVA-~i9+L`)NHyiA}C?IY@o7^_?doV%IyNsNc7j;;KrGY*TcUgmFVY6?`t zSXicbEYw4(ap8HMu*t`ZbgFANuQJ9l($N$nZAIqK3T_nu?!EWklYqt=3jptp{l)p3 z2~75c9zMo5A2MW!z`3DD{LIU9RCHj{{jv<@nEE!JKa6qBiXU@#9eQWoWYcAM?J!-T zFIxtinf`d6L4i2HGqvG=@j1|0`&9SdwkN=~nXXLSG*#d33D&4lqY8rGv_`bepTpXK z^Ht|yJ$NAUWl&%ap#QrL9Xj+9#Flysx|mq;0{HzlfoTm5@*~NaX-s8()Xk>Ff6Ot* zm`E2mYdjK-UR%gFo{fq%|9L z*xxv7Tp+Hpww%|;rYrXV!uCJ;-sY2M!p(Hj1>b!w?KV9F&UXA_%9JU)dA#8j`@LHM zQ5EoJ(8e6#IKe&w8CtosjjhyOn$}GweDqPo*-u(IQWK|5n-2$^`wimo8nZ>v|`RdI9cphS{)kO-^5dg$oxN zap_*rzJ;+XO!Rr_te^hTix)4}p>fiN4I5^}!#FC8b6z5rXVoO+0AlY}V_p1-*YE1} z?6c1{_P_UndV1P9%X>Urv*V0%XU(qan!IvRuf)&&d+3gRofhSjF=P>D*+OyDdFV%D&HrvHN*nOX!j&oV*QgZ;dd_D19HJ*FTb(G0|q&>w85r-gRKpC*8(4wBRX(5Wu&u% zp>s8zeZkk>A8njJ`Q(#!Bc}ANr7f$;F506T*OsfVzIrgO8LR>vm_2*;x^7%CUpCRK zynFha;Qa7Bd#O?jF!q=@_spYDM>BL^Lsx+=%9n>rWoGgyT=0C}rI{Qq0f#5l6yoR>;l*0at!OVDp={P^*L`%klG&2ska zIVRfvxEDoRYS=ak+D7vXfU=9KacwJ#Yr$eeGRVyLRpR73_8y=&hm*x+QC(g#)yWhaGC9bytby zx`i?PGabLB^UCzqlTSW*DEwZZM)MQp`W8s@^sb1_KNV*T zGUjmx83)>o1JJd0b!}h5;={He-pT`rOSOKR5t~(9wj4kl;LX-sZ@oESK;BqtN<-}a zFAz6t%d%x!*6@;ZfOb!S4^1*@OQ+2&d;$8Doan5=vgAO&e*K=e$z#|%oFny8Su!qb zbV)e?yTKoY$zqiE@ZmbLq=aXcl^qA*XUg-+v~yIR+jIW;=ZldeM>;-*w%C~4ZoBPk zWyid%!6oCsf&~k@A@=GwI$7m;nreJ&_(loDF4oF5aR9oMjQFggvf=>j01u*!>HbXT zRb7Am_1gTzLHoe6Vqn(Tl5qfMs+>p})BQ1iuMVDg;t6ej;ute#%&{dSKC7s#IADuG zY&weow%A5l9XD=VcCJ)eNl@t;J7dO-zNk$rd;R8Q_YDmM;b z58wlnyuv?PFmBqSMT?yCD{W^1(pRdqb!V9l6$!eT*;JLoElxMY-8wcpu zYK!e2MP8xzYC^2njmnL8S$j**0oxf`5oEOmaRJXMJ=Ix_<;VdnqAE^1?X=q?$YtI- zY~a9wBg>IzS)1FI0~cR>@i3e>@Ld#{<#&JZ;K5h5EyY>w<;;PpQ>X5ZGpC-h$ZHGE zP#MpB<;%NfEc_2@&IzOEUru&E*SK`Ml zAU}V-lkQf~=;v_I`xUHAryV|^sufYApWmt0uXtrT?i^6_H@N9_&WlDLPraL7tE9Wh zxtd_QOQuJtkH;m`Bh*iK$@B;fO-HA+LPfA%6m%h&-ryzY>iDA3YnLg#-p^kf$Ztyb zv$ry$(Zg>@_u1nz$jfg^_u1w$&dYB~_u1(()XN_?-Dk6cM@n}(z{6jtbl~$r=6{yn zI#RS19}3mK5%4%;F4~FiA11VN+ft!VZ_b(0>!f;}k zuC6(^X={3gl%C7gHM!~46_t%%c3jc=*8J7=l@cR1t!wPU^ij=7_Zhuf?$o-5zDRGb zY^iEknO<4ZSXX}m(i1WYwafrK0gvre9grRKq2p z12v+)xuW6J)^xC{zBL(N)S*pPPWq;`?1~0Ay-rjts;+O$ERxe5o;dszO}O{*Ful1oy%{(I>F9X2ix5Z`Lk;OK8PY}KbQE;KAp?)j)n|V$ zSHlD^-Djuro%P92`5i<(KFIZE>8<_JR(z0#LkRs!c`&7$1lj(8OIXnPRy7fjh zwc_FM16Y~va?V{itxR`0SL0-t^1Hv_+Pw4zr-Hh?bf5&ZCd+snf?P7?bf;p?I?fr< zYxD64wAkf>+%9BXxlEMtW7u?=fGi-+#fVE=rE^?&AwW;#O4 Date: Sun, 31 Jan 2021 15:27:00 +0100 Subject: [PATCH 288/563] Add test for UI methods --- freqtrade/commands/deploy_commands.py | 2 +- tests/rpc/test_rpc_apiserver.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 62da6abf3..797e9f87d 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -146,7 +146,7 @@ def clean_ui_subdir(directory: Path): logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root - if p.name in ('.gitkeep', 'fallback_file.html'): + if p.name in ('favicon.ico', 'fallback_file.html'): continue if p.is_file(): p.unlink() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 919481598..586af6dc1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -88,6 +88,21 @@ def test_api_not_found(botclient): assert rc.json() == {"detail": "Not Found"} +def test_api_ui_fallback(botclient): + ftbot, client = botclient + + rc = client_get(client, "/favicon.ico") + assert rc.status_code == 200 + + rc = client_get(client, "/fallback_file.html") + assert rc.status_code == 200 + assert '`freqtrade install-ui`' in rc.text + + # Forwarded to fallback_html or index.html (depending if it's installed or not) + rc = client_get(client, "/something") + assert rc.status_code == 200 + + def test_api_auth(): with pytest.raises(ValueError): create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234', token_type="NotATokenType") From 06e2bc94c3e111bcea180485018fd081da176104 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 15:37:57 +0100 Subject: [PATCH 289/563] Deploy to subdirectory --- freqtrade/commands/deploy_commands.py | 5 +++-- freqtrade/rpc/api_server/ui/installed/.gitkeep | 0 freqtrade/rpc/api_server/web_ui.py | 4 ++-- tests/commands/test_commands.py | 3 +++ 4 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 freqtrade/rpc/api_server/ui/installed/.gitkeep diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 797e9f87d..5ba3db9f9 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -146,7 +146,7 @@ def clean_ui_subdir(directory: Path): logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root - if p.name in ('favicon.ico', 'fallback_file.html'): + if p.name in ('.gitkeep', 'fallback_file.html'): continue if p.is_file(): p.unlink() @@ -169,6 +169,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str): logger.info(f"Downloading {dl_url}") resp = requests.get(dl_url).content + dest_folder.mkdir(parents=True, exist_ok=True) with ZipFile(BytesIO(resp)) as zf: for fn in zf.filelist: with zf.open(fn) as x: @@ -207,7 +208,7 @@ def get_ui_download_url() -> Tuple[str, str]: def start_install_ui(args: Dict[str, Any]) -> None: - dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/' # First make sure the assets are removed. dl_url, latest_version = get_ui_download_url() diff --git a/freqtrade/rpc/api_server/ui/installed/.gitkeep b/freqtrade/rpc/api_server/ui/installed/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 4876c9077..6d7e77953 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -20,12 +20,12 @@ async def index_html(rest_of_path: str): """ if rest_of_path.startswith('api') or rest_of_path.startswith('.'): raise HTTPException(status_code=404, detail="Not Found") - uibase = Path(__file__).parent / 'ui' + uibase = Path(__file__).parent / 'ui/installed/' if (uibase / rest_of_path).is_file(): return FileResponse(str(uibase / rest_of_path)) index_file = uibase / 'index.html' if not index_file.is_file(): - return FileResponse(str(uibase / 'fallback_file.html')) + return FileResponse(str(uibase.parent / 'fallback_file.html')) # Fall back to index.html, as indicated by vue router docs return FileResponse(str(index_file)) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 0d2b9e394..cec0b168e 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -622,6 +622,9 @@ def test_download_and_install_ui(mocker, tmpdir): folder = Path(tmpdir) / "uitests_dl" folder.mkdir(exist_ok=True) + + assert read_ui_version(folder) is None + download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip', '22') assert wb_mock.call_count == 2 From 2c80388b40bca3a77d765801eb96722f992340ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 19:49:14 +0100 Subject: [PATCH 290/563] Fix valueerror in case of empty array files --- freqtrade/data/history/jsondatahandler.py | 8 ++++++-- tests/commands/test_commands.py | 2 +- tests/data/test_history.py | 14 +++++++++++++- tests/rpc/test_rpc_apiserver.py | 2 +- tests/testdata/NOPAIR_XXX-4m.json | 1 + 5 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 tests/testdata/NOPAIR_XXX-4m.json diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 9122170d5..301d228a8 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -86,8 +86,12 @@ class JsonDataHandler(IDataHandler): filename = self._pair_data_filename(self._datadir, pair, timeframe) if not filename.exists(): return DataFrame(columns=self._columns) - pairdata = read_json(filename, orient='values') - pairdata.columns = self._columns + try: + pairdata = read_json(filename, orient='values') + pairdata.columns = self._columns + except ValueError: + logger.error(f"Could not load data for {pair}.") + return DataFrame(columns=self._columns) pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', 'volume': 'float'}) pairdata['date'] = to_datetime(pairdata['date'], diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index f8ecc8218..d655174b3 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1181,7 +1181,7 @@ def test_start_list_data(testdatadir, capsys): pargs['config'] = None start_list_data(pargs) captured = capsys.readouterr() - assert "Found 16 pair / timeframe combinations." in captured.out + assert "Found 17 pair / timeframe combinations." in captured.out assert "\n| Pair | Timeframe |\n" in captured.out assert "\n| UNITTEST/BTC | 1m, 5m, 8m, 30m |\n" in captured.out diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 99b22adda..353cfc6f7 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -646,7 +646,7 @@ def test_datahandler_ohlcv_get_available_data(testdatadir): ('ZEC/BTC', '5m'), ('UNITTEST/BTC', '1m'), ('ADA/BTC', '5m'), ('ETC/BTC', '5m'), ('NXT/BTC', '5m'), ('DASH/BTC', '5m'), ('XRP/ETH', '1m'), ('XRP/ETH', '5m'), ('UNITTEST/BTC', '30m'), - ('UNITTEST/BTC', '8m')} + ('UNITTEST/BTC', '8m'), ('NOPAIR/XXX', '4m')} paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir) assert set(paircombs) == {('UNITTEST/BTC', '8m')} @@ -672,6 +672,18 @@ def test_jsondatahandler_ohlcv_purge(mocker, testdatadir): assert unlinkmock.call_count == 1 +def test_jsondatahandler_ohlcv_load(testdatadir, caplog): + dh = JsonDataHandler(testdatadir) + df = dh.ohlcv_load('XRP/ETH', '5m') + assert len(df) == 711 + + # Failure case (empty array) + df1 = dh.ohlcv_load('NOPAIR/XXX', '4m') + assert len(df1) == 0 + assert log_has("Could not load data for NOPAIR/XXX.", caplog) + assert df.columns.equals(df1.columns) + + def test_jsondatahandler_trades_load(testdatadir, caplog): dh = JsonGzDataHandler(testdatadir) logmsg = "Old trades format detected - converting" diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b050e5694..71f1b3172 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1109,7 +1109,7 @@ def test_list_available_pairs(botclient): rc = client_get(client, f"{BASE_URI}/available_pairs") assert_response(rc) - assert rc.json()['length'] == 12 + assert rc.json()['length'] == 13 assert isinstance(rc.json()['pairs'], list) rc = client_get(client, f"{BASE_URI}/available_pairs?timeframe=5m") diff --git a/tests/testdata/NOPAIR_XXX-4m.json b/tests/testdata/NOPAIR_XXX-4m.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/testdata/NOPAIR_XXX-4m.json @@ -0,0 +1 @@ +[] From 76312d722a89ee88eb0b41e5c82224af1471a9bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 05:41:53 +0000 Subject: [PATCH 291/563] Bump pytest from 6.2.1 to 6.2.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.1 to 6.2.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.1...6.2.2) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 01066959a..fa0ead603 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.790 -pytest==6.2.1 +pytest==6.2.2 pytest-asyncio==0.14.0 pytest-cov==2.11.1 pytest-mock==3.5.1 From ed1d4f0568985b456bdf293f7d5ded22e1644e77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 05:42:05 +0000 Subject: [PATCH 292/563] Bump jinja2 from 2.11.2 to 2.11.3 Bumps [jinja2](https://github.com/pallets/jinja) from 2.11.2 to 2.11.3. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/master/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/2.11.2...2.11.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c6cb3e445..9c03f1087 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ jsonschema==3.2.0 TA-Lib==0.4.19 tabulate==0.8.7 pycoingecko==1.4.0 -jinja2==2.11.2 +jinja2==2.11.3 tables==3.6.1 blosc==1.10.2 From aa7120f27c318458c44926188619d242865a82e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 05:42:06 +0000 Subject: [PATCH 293/563] Bump ccxt from 1.40.99 to 1.41.35 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.40.99 to 1.41.35. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.40.99...1.41.35) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c6cb3e445..4d69cff0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.0 pandas==1.2.1 -ccxt==1.40.99 +ccxt==1.41.35 aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 From 2a5e0920ecec6dcb74afc5e552dcf44c468a8def Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 05:42:08 +0000 Subject: [PATCH 294/563] Bump mkdocs-material from 6.2.5 to 6.2.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.2.5 to 6.2.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.2.5...6.2.7) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 6fef05f0c..8886fd287 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.2.5 +mkdocs-material==6.2.7 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1 From 7fcf0d52319c81ec44c25d45321840dfa6ea63fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 08:45:31 +0000 Subject: [PATCH 295/563] Bump pymdown-extensions from 8.1 to 8.1.1 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 8.1 to 8.1.1. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/8.1...8.1.1) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 8886fd287..85bd72323 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ mkdocs-material==6.2.7 mdx_truly_sane_lists==1.2 -pymdown-extensions==8.1 +pymdown-extensions==8.1.1 From ccdac3d4c3e1ae8f83e8acb8fc47b6d8dd4a6d20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 08:50:24 +0000 Subject: [PATCH 296/563] Bump urllib3 from 1.26.2 to 1.26.3 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.2 to 1.26.3. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/1.26.3/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.2...1.26.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eff13cd57..e247d4b00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ python-telegram-bot==13.1 arrow==0.17.0 cachetools==4.2.1 requests==2.25.1 -urllib3==1.26.2 +urllib3==1.26.3 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 From 4facf662de3dacd83fa15cee0004bcb90dea4e6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Feb 2021 11:00:55 +0100 Subject: [PATCH 297/563] Fix random test-failure caused by un-clean hyperopt shutdown pytest --random-order-seed=415781 --- tests/optimize/test_hyperopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 89c7305b6..68eb3d6f7 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -251,9 +251,9 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: - start_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf))) + hyperopt_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf))) patched_configuration_load_config_file(mocker, hyperopt_conf) - mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) + mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.__init__', hyperopt_mock) patch_exchange(mocker) args = [ From cd5c58fd370f4d11469d58dc01bf61637393b3fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Feb 2021 12:58:18 +0100 Subject: [PATCH 298/563] Properly patch exchange for plot_profit test --- tests/test_plotting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 4c8ac4816..1752f9b94 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -432,7 +432,8 @@ def test_start_plot_profit_error(mocker): start_plot_profit(argsp) -def test_plot_profit(default_conf, mocker, testdatadir, caplog): +def test_plot_profit(default_conf, mocker, testdatadir): + patch_exchange(mocker) default_conf['trade_source'] = 'file' default_conf['datadir'] = testdatadir default_conf['exportfilename'] = testdatadir / 'backtest-result_test_nofile.json' From 55c9489eb2907d2354a0bc51c493ebc8fa9fc74d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Feb 2021 15:10:55 +0100 Subject: [PATCH 299/563] Downgrade RPI docker-image to 3.7 otherwise piwheels.org does not work at the moment --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 2c7d4538a..b6f2e44e6 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.9.1-slim-buster as base +FROM --platform=linux/arm/v7 python:3.7.9-slim-buster as base # Setup env ENV LANG C.UTF-8 From 130a9b4db33afb9e71ffe917c70d301611d05e17 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Feb 2021 19:39:55 +0100 Subject: [PATCH 300/563] Add test to call verify_pairlist multiple times --- tests/plugins/test_pairlist.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index fda2b1409..67cd96f5b 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -126,6 +126,20 @@ def test_load_pairlist_noexist(mocker, markets, default_conf): default_conf, {}, 1) +def test_load_pairlist_verify_multi(mocker, markets, default_conf): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + plm = PairListManager(freqtrade.exchange, default_conf) + # Call different versions one after the other, should always consider what was passed in + # and have no side-effects (therefore the same check multiple times) + assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC'] + assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', 'BUUU/BTC'], print) == ['ETH/BTC', 'XRP/BTC'] + assert plm.verify_whitelist(['XRP/BTC', 'BUUU/BTC'], print) == ['XRP/BTC'] + assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC'] + assert plm.verify_whitelist(['ETH/USDT', 'XRP/USDT', ], print) == ['ETH/USDT', ] + assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC'] + + def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) From 52acf9aaf6648e7478acad42619f4222b6c01614 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Feb 2021 19:40:31 +0100 Subject: [PATCH 301/563] Fix "disappearing pairs" bug closes #4277 --- freqtrade/plugins/pairlist/IPairList.py | 2 +- freqtrade/plugins/pairlistmanager.py | 17 ++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 95d776ae6..184feff9e 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -168,7 +168,7 @@ class IPairList(LoggingMixin, ABC): # Check if market is active market = markets[pair] if not market_is_active(market): - logger.info(f"Ignoring {pair} from whitelist. Market is not active.") + self.log_once(f"Ignoring {pair} from whitelist. Market is not active.", logger.info) continue if pair not in sanitized_whitelist: sanitized_whitelist.append(pair) diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 7ce77da59..4e4135981 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -59,17 +59,6 @@ class PairListManager(): """The expanded blacklist (including wildcard expansion)""" return expand_pairlist(self._blacklist, self._exchange.get_markets().keys()) - @property - def expanded_whitelist_keep_invalid(self) -> List[str]: - """The expanded whitelist (including wildcard expansion), maintaining invalid pairs""" - return expand_pairlist(self._whitelist, self._exchange.get_markets().keys(), - keep_invalid=True) - - @property - def expanded_whitelist(self) -> List[str]: - """The expanded whitelist (including wildcard expansion), filtering invalid pairs""" - return expand_pairlist(self._whitelist, self._exchange.get_markets().keys()) - @property def name_list(self) -> List[str]: """Get list of loaded Pairlist Handler names""" @@ -153,10 +142,8 @@ class PairListManager(): :return: pairlist - whitelisted pairs """ try: - if keep_invalid: - whitelist = self.expanded_whitelist_keep_invalid - else: - whitelist = self.expanded_whitelist + + whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid) except ValueError as err: logger.error(f"Pair whitelist contains an invalid Wildcard: {err}") return [] From a9f1c871ddb68734ee179598dc08a21dda982f16 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Feb 2021 19:48:29 +0100 Subject: [PATCH 302/563] Add path loading snippet to derived strategies closes #4279 --- docs/strategy-advanced.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index ca20d3588..c051e2232 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -399,6 +399,17 @@ class MyAwesomeStrategy2(MyAwesomeStrategy): Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need. +!!! Note "Parent-strategy in different files" + If you have the parent-strategy in a different file, you'll need to add the following to the top of your "child"-file to ensure proper loading, otherwise freqtrade may not be able to load the parent strategy correctly. + + ``` python + import sys + from pathlib import Path + sys.path.append(str(Path(__file__).parent)) + + from myawesomestrategy import MyAwesomeStrategy + ``` + ## Embedding Strategies Freqtrade provides you with with an easy way to embed the strategy into your configuration file. From 3d9b4034e683e9985c2695418eedba7bb0ac7070 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Feb 2021 08:06:19 +0100 Subject: [PATCH 303/563] Use already calculated current_profit for sell_profit_offset comparison --- freqtrade/strategy/interface.py | 2 +- tests/test_freqtradebot.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 77d45b445..da4ce6c50 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -530,7 +530,7 @@ class IStrategy(ABC): current_time=date)) if (ask_strategy.get('sell_profit_only', False) - and trade.calc_profit_ratio(rate=rate) <= ask_strategy.get('sell_profit_offset', 0)): + and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal sell_signal = False else: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2408afc87..e2b70257a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3159,9 +3159,9 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': 0.00002172, - 'ask': 0.00002173, - 'last': 0.00002172 + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 }), buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, From cfd0bb896472b21b3a0cf8051f5e6f54d3e08296 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Feb 2021 19:47:21 +0100 Subject: [PATCH 304/563] Extract min stake amount from bot to exchange class --- freqtrade/exchange/exchange.py | 37 ++++++++- freqtrade/freqtradebot.py | 37 +-------- tests/exchange/test_exchange.py | 130 ++++++++++++++++++++++++++++++ tests/test_freqtradebot.py | 137 +------------------------------- 4 files changed, 170 insertions(+), 171 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 78e17359d..c7625b53c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -17,7 +17,7 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU decimal_to_precision) from pandas import DataFrame -from freqtrade.constants import ListPairsWithTimeframes +from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, RetryableOrderError, @@ -490,6 +490,41 @@ class Exchange: else: return 1 / pow(10, precision) + def get_min_pair_stake_amount(self, pair: str, price: float, + stoploss: float) -> Optional[float]: + try: + market = self.markets[pair] + except KeyError: + raise ValueError(f"Can't get market information for symbol {pair}") + + if 'limits' not in market: + return None + + min_stake_amounts = [] + limits = market['limits'] + if ('cost' in limits and 'min' in limits['cost'] + and limits['cost']['min'] is not None): + min_stake_amounts.append(limits['cost']['min']) + + if ('amount' in limits and 'min' in limits['amount'] + and limits['amount']['min'] is not None): + min_stake_amounts.append(limits['amount']['min'] * price) + + if not min_stake_amounts: + return None + + # reserve some percent defined in config (5% default) + stoploss + amount_reserve_percent = 1.0 - self._config.get('amount_reserve_percent', + DEFAULT_AMOUNT_RESERVE_PERCENT) + amount_reserve_percent += stoploss + # it should not be more than 50% + amount_reserve_percent = max(amount_reserve_percent, 0.5) + + # The value returned should satisfy both limits: for amount (base currency) and + # for cost (quote, stake currency), so max() is used here. + # See also #2575 at github. + return max(min_stake_amounts) / amount_reserve_percent + def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d7116834a..2656daab5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -516,40 +516,6 @@ class FreqtradeBot(LoggingMixin): return stake_amount - def _get_min_pair_stake_amount(self, pair: str, price: float) -> Optional[float]: - try: - market = self.exchange.markets[pair] - except KeyError: - raise ValueError(f"Can't get market information for symbol {pair}") - - if 'limits' not in market: - return None - - min_stake_amounts = [] - limits = market['limits'] - if ('cost' in limits and 'min' in limits['cost'] - and limits['cost']['min'] is not None): - min_stake_amounts.append(limits['cost']['min']) - - if ('amount' in limits and 'min' in limits['amount'] - and limits['amount']['min'] is not None): - min_stake_amounts.append(limits['amount']['min'] * price) - - if not min_stake_amounts: - return None - - # reserve some percent defined in config (5% default) + stoploss - amount_reserve_percent = 1.0 - self.config.get('amount_reserve_percent', - constants.DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent += self.strategy.stoploss - # it should not be more than 50% - amount_reserve_percent = max(amount_reserve_percent, 0.5) - - # The value returned should satisfy both limits: for amount (base currency) and - # for cost (quote, stake currency), so max() is used here. - # See also #2575 at github. - return max(min_stake_amounts) / amount_reserve_percent - def create_trade(self, pair: str) -> bool: """ Check the implemented trading strategy for buy signals. @@ -646,7 +612,8 @@ class FreqtradeBot(LoggingMixin): if not buy_limit_requested: raise PricingError('Could not determine buy price.') - min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested) + min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested, + self.strategy.stoploss) if min_stake_amount is not None and min_stake_amount > stake_amount: logger.warning( f"Can't open a new trade for {pair}: stake amount " diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a35dc9da9..cd24e113e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -305,6 +305,136 @@ def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precisio assert pytest.approx(exchange.price_get_one_pip(pair, price)) == expected +def test_get_min_pair_stake_amount(mocker, default_conf) -> None: + + exchange = get_patched_exchange(mocker, default_conf, id="binance") + stoploss = -0.05 + markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} + + # no pair found + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + with pytest.raises(ValueError, match=r'.*get market information.*'): + exchange.get_min_pair_stake_amount('BNB/BTC', 1, stoploss) + + # no 'limits' section + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result is None + + # empty 'limits' section + markets["ETH/BTC"]["limits"] = {} + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result is None + + # no cost Min + markets["ETH/BTC"]["limits"] = { + 'cost': {"min": None}, + 'amount': {} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result is None + + # no amount Min + markets["ETH/BTC"]["limits"] = { + 'cost': {}, + 'amount': {"min": None} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result is None + + # empty 'cost'/'amount' section + markets["ETH/BTC"]["limits"] = { + 'cost': {}, + 'amount': {} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result is None + + # min cost is set + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 2}, + 'amount': {} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result == 2 / 0.9 + + # min amount is set + markets["ETH/BTC"]["limits"] = { + 'cost': {}, + 'amount': {'min': 2} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) + assert result == 2 * 2 / 0.9 + + # min amount and cost are set (cost is minimal) + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 2}, + 'amount': {'min': 2} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) + assert result == max(2, 2 * 2) / 0.9 + + # min amount and cost are set (amount is minial) + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 8}, + 'amount': {'min': 2} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) + assert result == max(8, 2 * 2) / 0.9 + + +def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: + exchange = get_patched_exchange(mocker, default_conf, id="binance") + stoploss = -0.05 + markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} + + # Real Binance data + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 0.0001}, + 'amount': {'min': 0.001} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) + + def test_set_sandbox(default_conf, mocker): """ Test working scenario diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e2b70257a..abb91d66b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -394,139 +394,6 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: assert Trade.total_open_trades_stakes() == 1.97502e-03 -def test_get_min_pair_stake_amount(mocker, default_conf) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.stoploss = -0.05 - markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} - - # no pair found - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - with pytest.raises(ValueError, match=r'.*get market information.*'): - freqtrade._get_min_pair_stake_amount('BNB/BTC', 1) - - # no 'limits' section - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result is None - - # empty 'limits' section - markets["ETH/BTC"]["limits"] = {} - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result is None - - # no cost Min - markets["ETH/BTC"]["limits"] = { - 'cost': {"min": None}, - 'amount': {} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result is None - - # no amount Min - markets["ETH/BTC"]["limits"] = { - 'cost': {}, - 'amount': {"min": None} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result is None - - # empty 'cost'/'amount' section - markets["ETH/BTC"]["limits"] = { - 'cost': {}, - 'amount': {} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result is None - - # min cost is set - markets["ETH/BTC"]["limits"] = { - 'cost': {'min': 2}, - 'amount': {} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result == 2 / 0.9 - - # min amount is set - markets["ETH/BTC"]["limits"] = { - 'cost': {}, - 'amount': {'min': 2} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) - assert result == 2 * 2 / 0.9 - - # min amount and cost are set (cost is minimal) - markets["ETH/BTC"]["limits"] = { - 'cost': {'min': 2}, - 'amount': {'min': 2} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) - assert result == max(2, 2 * 2) / 0.9 - - # min amount and cost are set (amount is minial) - markets["ETH/BTC"]["limits"] = { - 'cost': {'min': 8}, - 'amount': {'min': 2} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) - assert result == max(8, 2 * 2) / 0.9 - - -def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.stoploss = -0.05 - markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} - - # Real Binance data - markets["ETH/BTC"]["limits"] = { - 'cost': {'min': 0.0001}, - 'amount': {'min': 0.001} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 0.020405) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) - - def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -1007,7 +874,6 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', get_buy_rate=buy_rate_mock, - _get_min_pair_stake_amount=MagicMock(return_value=1) ) buy_mm = MagicMock(return_value=limit_buy_order_open) mocker.patch.multiple( @@ -1018,6 +884,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order 'last': 0.00001172 }), buy=buy_mm, + get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, ) pair = 'ETH/BTC' @@ -1112,7 +979,6 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) - mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', get_buy_rate=MagicMock(return_value=0.11), - _get_min_pair_stake_amount=MagicMock(return_value=1) ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1122,6 +988,7 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) - 'last': 0.00001172 }), buy=MagicMock(return_value=limit_buy_order), + get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, ) stake_amount = 2 From f0532f28cf01b03d05c332a1f2a5e13475bd510d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Feb 2021 20:03:01 +0100 Subject: [PATCH 305/563] Small doc-reorg adding "advanced topics" as main header --- docs/bot-basics.md | 1 + mkdocs.yml | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 86fb18645..30a25d4fc 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -4,6 +4,7 @@ This page provides you some basic concepts on how Freqtrade works and operates. ## Freqtrade terminology +* Strategy: Your trading strategy, telling the bot what to do. * Trade: Open position. * Open Order: Order which is currently placed on the exchange, and is not yet complete. * Pair: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT). diff --git a/mkdocs.yml b/mkdocs.yml index 4545e8d84..47ab8ec48 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,7 @@ nav: - Freqtrade Basics: bot-basics.md - Configuration: configuration.md - Strategy Customization: strategy-customization.md + - Plugins: plugins.md - Stoploss: stoploss.md - Start the bot: bot-usage.md - Control the bot: @@ -17,20 +18,20 @@ nav: - Data Downloading: data-download.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md - - Edge Positioning: edge.md - - Plugins: plugins.md - - Utility Subcommands: utils.md - - FAQ: faq.md + - Utility Sub-commands: utils.md - Data Analysis: - Jupyter Notebooks: data-analysis.md - Strategy analysis: strategy_analysis_example.md - Plotting: plotting.md - - SQL Cheatsheet: sql_cheatsheet.md - Exchange-specific Notes: exchanges.md - - Advanced Post-installation Tasks: advanced-setup.md - - Advanced Strategy: strategy-advanced.md - - Advanced Hyperopt: advanced-hyperopt.md - - Sandbox Testing: sandbox-testing.md + - Advanced Topics: + - Advanced Post-installation Tasks: advanced-setup.md + - Edge Positioning: edge.md + - Advanced Strategy: strategy-advanced.md + - Advanced Hyperopt: advanced-hyperopt.md + - Sandbox Testing: sandbox-testing.md + - FAQ: faq.md + - SQL Cheat-sheet: sql_cheatsheet.md - Updating Freqtrade: updating.md - Deprecated Features: deprecated.md - Contributors Guide: developer.md From 3e3c9e99c75a8efb6b49c684a337b1ec5cfd8045 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Feb 2021 20:03:28 +0100 Subject: [PATCH 306/563] Move command references to their respective subpages --- docs/backtesting.md | 83 +++++++++++++++ docs/bot-usage.md | 252 -------------------------------------------- docs/edge.md | 55 ++++++++++ docs/hyperopt.md | 101 ++++++++++++++++++ 4 files changed, 239 insertions(+), 252 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index e1ab1c72d..15b189ebf 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -5,6 +5,89 @@ This page explains how to validate your strategy performance by using Backtestin Backtesting requires historic data to be available. To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. +## Backtesting command reference + +``` +usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] [-s NAME] + [--strategy-path PATH] [-i TIMEFRAME] + [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] + [--max-open-trades INT] + [--stake-amount STAKE_AMOUNT] [--fee FLOAT] + [--eps] [--dmmp] [--enable-protections] + [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] + [--export EXPORT] [--export-filename PATH] + +optional arguments: + -h, --help show this help message and exit + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). + --timerange TIMERANGE + Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. + --fee FLOAT Specify fee ratio. Will be applied twice (on trade + entry and exit). + --eps, --enable-position-stacking + Allow buying the same pair multiple times (position + stacking). + --dmmp, --disable-max-market-positions + Disable applying `max_open_trades` during backtest + (same as setting `max_open_trades` to a very high + number). + --enable-protections, --enableprotections + Enable protections for backtesting.Will slow + backtesting down by a considerable amount, but will + include configured protections + --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] + Provide a space-separated list of strategies to + backtest. Please note that ticker-interval needs to be + set either in config or via command line. When using + this together with `--export trades`, the strategy- + name is injected into the filename (so `backtest- + data.json` becomes `backtest-data- + DefaultStrategy.json` + --export EXPORT Export backtest results, argument are: trades. + Example: `--export=trades` + --export-filename PATH + Save backtest results to the file with this filename. + Requires `--export` to be set as well. Example: + `--export-filename=user_data/backtest_results/backtest + _today.json` + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. + --strategy-path PATH Specify additional strategy lookup path. + +``` + ## Test your strategy with Backtesting Now you have good Buy and Sell strategies and some historic data, you want to test it against diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 5820b3cc7..c7fe8634d 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -205,258 +205,6 @@ in production mode. Example command: freqtrade trade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite ``` -## Backtesting commands - -Backtesting also uses the config specified via `-c/--config`. - -``` -usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] - [-d PATH] [--userdir PATH] [-s NAME] - [--strategy-path PATH] [-i TIMEFRAME] - [--timerange TIMERANGE] - [--data-format-ohlcv {json,jsongz,hdf5}] - [--max-open-trades INT] - [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--eps] [--dmmp] [--enable-protections] - [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] - [--export EXPORT] [--export-filename PATH] - -optional arguments: - -h, --help show this help message and exit - -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). - --timerange TIMERANGE - Specify what timerange of data to use. - --data-format-ohlcv {json,jsongz,hdf5} - Storage format for downloaded candle (OHLCV) data. - (default: `None`). - --max-open-trades INT - Override the value of the `max_open_trades` - configuration setting. - --stake-amount STAKE_AMOUNT - Override the value of the `stake_amount` configuration - setting. - --fee FLOAT Specify fee ratio. Will be applied twice (on trade - entry and exit). - --eps, --enable-position-stacking - Allow buying the same pair multiple times (position - stacking). - --dmmp, --disable-max-market-positions - Disable applying `max_open_trades` during backtest - (same as setting `max_open_trades` to a very high - number). - --enable-protections, --enableprotections - Enable protections for backtesting.Will slow - backtesting down by a considerable amount, but will - include configured protections - --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] - Provide a space-separated list of strategies to - backtest. Please note that ticker-interval needs to be - set either in config or via command line. When using - this together with `--export trades`, the strategy- - name is injected into the filename (so `backtest- - data.json` becomes `backtest-data- - DefaultStrategy.json` - --export EXPORT Export backtest results, argument are: trades. - Example: `--export=trades` - --export-filename PATH - Save backtest results to the file with this filename. - Requires `--export` to be set as well. Example: - `--export-filename=user_data/backtest_results/backtest - _today.json` - -Common arguments: - -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: - 'syslog', 'journald'. See the documentation for more - details. - -V, --version show program's version number and exit - -c PATH, --config PATH - Specify configuration file (default: - `userdir/config.json` or `config.json` whichever - exists). Multiple --config options may be used. Can be - set to `-` to read config from stdin. - -d PATH, --datadir PATH - Path to directory with historical backtesting data. - --userdir PATH, --user-data-dir PATH - Path to userdata directory. - -Strategy arguments: - -s NAME, --strategy NAME - Specify strategy class name which will be used by the - bot. - --strategy-path PATH Specify additional strategy lookup path. - -``` - -### Getting historic data for backtesting - -The first time your run Backtesting, you will need to download some historic data first. -This can be accomplished by using `freqtrade download-data`. -Check the corresponding [Data Downloading](data-download.md) section for more details - -## Hyperopt commands - -To optimize your strategy, you can use hyperopt parameter hyperoptimization -to find optimal parameter values for your strategy. - -``` -usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] - [--userdir PATH] [-s NAME] [--strategy-path PATH] - [-i TIMEFRAME] [--timerange TIMERANGE] - [--data-format-ohlcv {json,jsongz,hdf5}] - [--max-open-trades INT] - [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [--dmmp] [--enable-protections] [-e INT] - [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] - [--print-all] [--no-color] [--print-json] [-j JOBS] - [--random-state INT] [--min-trades INT] - [--hyperopt-loss NAME] - -optional arguments: - -h, --help show this help message and exit - -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). - --timerange TIMERANGE - Specify what timerange of data to use. - --data-format-ohlcv {json,jsongz,hdf5} - Storage format for downloaded candle (OHLCV) data. - (default: `None`). - --max-open-trades INT - Override the value of the `max_open_trades` - configuration setting. - --stake-amount STAKE_AMOUNT - Override the value of the `stake_amount` configuration - setting. - --fee FLOAT Specify fee ratio. Will be applied twice (on trade - entry and exit). - --hyperopt NAME Specify hyperopt class name which will be used by the - bot. - --hyperopt-path PATH Specify additional lookup path for Hyperopt and - Hyperopt Loss functions. - --eps, --enable-position-stacking - Allow buying the same pair multiple times (position - stacking). - --dmmp, --disable-max-market-positions - Disable applying `max_open_trades` during backtest - (same as setting `max_open_trades` to a very high - number). - --enable-protections, --enableprotections - Enable protections for backtesting.Will slow - backtesting down by a considerable amount, but will - include configured protections - -e INT, --epochs INT Specify number of epochs (default: 100). - --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] - Specify which parameters to hyperopt. Space-separated - list. - --print-all Print all results, not only the best ones. - --no-color Disable colorization of hyperopt results. May be - useful if you are redirecting output to a file. - --print-json Print output in JSON format. - -j JOBS, --job-workers JOBS - The number of concurrently running jobs for - hyperoptimization (hyperopt worker processes). If -1 - (default), all CPUs are used, for -2, all CPUs but one - are used, etc. If 1 is given, no parallel computing - code is used at all. - --random-state INT Set random state to some positive integer for - reproducible hyperopt results. - --min-trades INT Set minimal desired number of trades for evaluations - in the hyperopt optimization path (default: 1). - --hyperopt-loss NAME Specify the class name of the hyperopt loss function - class (IHyperOptLoss). Different functions can - generate completely different results, since the - target for optimization is different. Built-in - Hyperopt-loss-functions are: - ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, - SharpeHyperOptLoss, SharpeHyperOptLossDaily, - SortinoHyperOptLoss, SortinoHyperOptLossDaily - -Common arguments: - -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: - 'syslog', 'journald'. See the documentation for more - details. - -V, --version show program's version number and exit - -c PATH, --config PATH - Specify configuration file (default: - `userdir/config.json` or `config.json` whichever - exists). Multiple --config options may be used. Can be - set to `-` to read config from stdin. - -d PATH, --datadir PATH - Path to directory with historical backtesting data. - --userdir PATH, --user-data-dir PATH - Path to userdata directory. - -Strategy arguments: - -s NAME, --strategy NAME - Specify strategy class name which will be used by the - bot. - --strategy-path PATH Specify additional strategy lookup path. - -``` - -## Edge commands - -To know your trade expectancy and winrate against historical data, you can use Edge. - -``` -usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] - [--userdir PATH] [-s NAME] [--strategy-path PATH] - [-i TIMEFRAME] [--timerange TIMERANGE] - [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] - [--fee FLOAT] [--stoplosses STOPLOSS_RANGE] - -optional arguments: - -h, --help show this help message and exit - -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). - --timerange TIMERANGE - Specify what timerange of data to use. - --max-open-trades INT - Override the value of the `max_open_trades` - configuration setting. - --stake-amount STAKE_AMOUNT - Override the value of the `stake_amount` configuration - setting. - --fee FLOAT Specify fee ratio. Will be applied twice (on trade - entry and exit). - --stoplosses STOPLOSS_RANGE - Defines a range of stoploss values against which edge - will assess the strategy. The format is "min,max,step" - (without any space). Example: - `--stoplosses=-0.01,-0.1,-0.001` - -Common arguments: - -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: - 'syslog', 'journald'. See the documentation for more - details. - -V, --version show program's version number and exit - -c PATH, --config PATH - Specify configuration file (default: - `userdir/config.json` or `config.json` whichever - exists). Multiple --config options may be used. Can be - set to `-` to read config from stdin. - -d PATH, --datadir PATH - Path to directory with historical backtesting data. - --userdir PATH, --user-data-dir PATH - Path to userdata directory. - -Strategy arguments: - -s NAME, --strategy NAME - Specify strategy class name which will be used by the - bot. - --strategy-path PATH Specify additional strategy lookup path. - -``` - -To understand edge and how to read the results, please read the [edge documentation](edge.md). - ## Next step The optimal strategy of the bot will change with time depending of the market trends. The next step is to diff --git a/docs/edge.md b/docs/edge.md index 6f01fcf65..5565ca2f9 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -9,6 +9,7 @@ The `Edge Positioning` module uses probability to calculate your win rate and ri `Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. `Edge Positioning` improves the performance of some trading strategies and *decreases* the performance of others. + ## Introduction Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose. @@ -208,6 +209,60 @@ Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. - **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2\%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**. +## Edge command reference + +``` +usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [-s NAME] [--strategy-path PATH] + [-i TIMEFRAME] [--timerange TIMERANGE] + [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] + [--fee FLOAT] [--stoplosses STOPLOSS_RANGE] + +optional arguments: + -h, --help show this help message and exit + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). + --timerange TIMERANGE + Specify what timerange of data to use. + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. + --fee FLOAT Specify fee ratio. Will be applied twice (on trade + entry and exit). + --stoplosses STOPLOSS_RANGE + Defines a range of stoploss values against which edge + will assess the strategy. The format is "min,max,step" + (without any space). Example: + `--stoplosses=-0.01,-0.1,-0.001` + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. + --strategy-path PATH Specify additional strategy lookup path. + +``` + ## Configurations Edge module has following configuration options: diff --git a/docs/hyperopt.md b/docs/hyperopt.md index f88d9cd4f..ec155062f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -32,6 +32,107 @@ source .env/bin/activate pip install -r requirements-hyperopt.txt ``` +## Hyperopt command reference + + +``` +usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [-s NAME] [--strategy-path PATH] + [-i TIMEFRAME] [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] + [--max-open-trades INT] + [--stake-amount STAKE_AMOUNT] [--fee FLOAT] + [--hyperopt NAME] [--hyperopt-path PATH] [--eps] + [--dmmp] [--enable-protections] [-e INT] + [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] + [--print-all] [--no-color] [--print-json] [-j JOBS] + [--random-state INT] [--min-trades INT] + [--hyperopt-loss NAME] + +optional arguments: + -h, --help show this help message and exit + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). + --timerange TIMERANGE + Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. + --fee FLOAT Specify fee ratio. Will be applied twice (on trade + entry and exit). + --hyperopt NAME Specify hyperopt class name which will be used by the + bot. + --hyperopt-path PATH Specify additional lookup path for Hyperopt and + Hyperopt Loss functions. + --eps, --enable-position-stacking + Allow buying the same pair multiple times (position + stacking). + --dmmp, --disable-max-market-positions + Disable applying `max_open_trades` during backtest + (same as setting `max_open_trades` to a very high + number). + --enable-protections, --enableprotections + Enable protections for backtesting.Will slow + backtesting down by a considerable amount, but will + include configured protections + -e INT, --epochs INT Specify number of epochs (default: 100). + --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] + Specify which parameters to hyperopt. Space-separated + list. + --print-all Print all results, not only the best ones. + --no-color Disable colorization of hyperopt results. May be + useful if you are redirecting output to a file. + --print-json Print output in JSON format. + -j JOBS, --job-workers JOBS + The number of concurrently running jobs for + hyperoptimization (hyperopt worker processes). If -1 + (default), all CPUs are used, for -2, all CPUs but one + are used, etc. If 1 is given, no parallel computing + code is used at all. + --random-state INT Set random state to some positive integer for + reproducible hyperopt results. + --min-trades INT Set minimal desired number of trades for evaluations + in the hyperopt optimization path (default: 1). + --hyperopt-loss NAME Specify the class name of the hyperopt loss function + class (IHyperOptLoss). Different functions can + generate completely different results, since the + target for optimization is different. Built-in + Hyperopt-loss-functions are: + ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, + SharpeHyperOptLoss, SharpeHyperOptLossDaily, + SortinoHyperOptLoss, SortinoHyperOptLossDaily + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. + --strategy-path PATH Specify additional strategy lookup path. + +``` + ## Prepare Hyperopting Before we start digging into Hyperopt, we recommend you to take a look at From dabe456d65a2dd1365b0fb83c18e60ae1f483f2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Feb 2021 20:20:34 +0100 Subject: [PATCH 307/563] Improve wording of configuration doc remove unneeded sections --- docs/configuration.md | 39 +++++++++------------------------------ 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 509214b9c..06619b301 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -111,12 +111,12 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.
*Defaults to `freqtrade`*
**Datatype:** String | `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances.
**Datatype:** String, SQLAlchemy connect string -| `initial_state` | Defines the initial application state. More information below.
*Defaults to `stopped`.*
**Datatype:** Enum, either `stopped` or `running` +| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command.
*Defaults to `stopped`.*
**Datatype:** Enum, either `stopped` or `running` | `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below.
**Datatype:** Boolean | `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `False`*.
**Datatype:** Boolean | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`.
**Datatype:** ClassName | `strategy_path` | Adds an additional strategy lookup path (must be a directory).
**Datatype:** String -| `internals.process_throttle_secs` | Set the process throttle. Value in second.
*Defaults to `5` seconds.*
**Datatype:** Positive Integer +| `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second.
*Defaults to `5` seconds.*
**Datatype:** Positive Integer | `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages.
*Defaults to `60` seconds.*
**Datatype:** Positive Integer or 0 | `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
**Datatype:** Boolean | `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file.
**Datatype:** String @@ -246,38 +246,16 @@ If it is not set in either Strategy or Configuration, a default of 1000% `{"0": !!! Note "Special case to forcesell after a specific time" A special case presents using `"": -1` as ROI. This forces the bot to sell a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-sell. -### Understand stoploss - -Go to the [stoploss documentation](stoploss.md) for more details. - -### Understand trailing stoploss - -Go to the [trailing stoploss Documentation](stoploss.md#trailing-stop-loss) for details on trailing stoploss. - -### Understand initial_state - -The `initial_state` configuration parameter is an optional field that defines the initial application state. -Possible values are `running` or `stopped`. (default=`running`) -If the value is `stopped` the bot has to be started with `/start` first. - ### Understand forcebuy_enable -The `forcebuy_enable` configuration parameter enables the usage of forcebuy commands via Telegram. -This is disabled for security reasons by default, and will show a warning message on startup if enabled. -For example, you can send `/forcebuy ETH/BTC` Telegram command when this feature if enabled to the bot, -who then buys the pair and holds it until a regular sell-signal (ROI, stoploss, /forcesell) appears. +The `forcebuy_enable` configuration parameter enables the usage of forcebuy commands via Telegram and REST API. +For security reasons, it's disabled by default, and freqtrade will show a warning message on startup if enabled. +For example, you can send `/forcebuy ETH/BTC` to the bot, which will result in freqtrade buying the pair and holds it until a regular sell-signal (ROI, stoploss, /forcesell) appears. This can be dangerous with some strategies, so use with care. See [the telegram documentation](telegram-usage.md) for details on usage. -### Understand process_throttle_secs - -The `process_throttle_secs` configuration parameter is an optional field that defines in seconds how long the bot should wait -before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for -every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or -the static list of pairs) if we should buy. - ### Ignoring expired candles When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it. @@ -600,7 +578,7 @@ Obviously, if only one side is using limit orders, different pricing combination --8<-- "includes/pairlists.md" --8<-- "includes/protections.md" -## Switch to Dry-run mode +## Using Dry-run mode We recommend starting the bot in the Dry-run mode to see how your bot will behave and what is the performance of your strategy. In the Dry-run mode the @@ -633,9 +611,10 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo ### Considerations for dry-run -* API-keys may or may not be provided. Only Read-Only operations (i.e. operations that do not alter account state) on the exchange are performed in the dry-run mode. -* Wallets (`/balance`) are simulated. +* API-keys may or may not be provided. Only Read-Only operations (i.e. operations that do not alter account state) on the exchange are performed in dry-run mode. +* Wallets (`/balance`) are simulated based on `dry_run_wallet`. * Orders are simulated, and will not be posted to the exchange. +* Orders are assumed to fill immediately, and will never time out. * In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled. * Open orders (not trades, which are stored in the database) are reset on bot restart. From b41078cc466973e67128d44269ac3d4b2f6f6815 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Feb 2021 20:23:30 +0100 Subject: [PATCH 308/563] Don't include plugin documentation in Configuration page --- docs/backtesting.md | 2 +- docs/configuration.md | 6 ++---- docs/developer.md | 2 +- docs/utils.md | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 15b189ebf..a14c8f2e4 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -103,7 +103,7 @@ The result of backtesting will confirm if your bot has better odds of making a p !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. - Please read the [pairlists documentation](configuration.md#pairlists) for more information. + Please read the [pairlists documentation](plugins.md#pairlists) for more information. To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. diff --git a/docs/configuration.md b/docs/configuration.md index 06619b301..e5eac973f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -91,8 +91,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean -| `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts -| `protections` | Define one or more protections to be used. [More information below](#protections). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** List of Dicts +| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts +| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** List of Dicts | `telegram.enabled` | Enable the usage of Telegram.
**Datatype:** Boolean | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String @@ -575,8 +575,6 @@ Assuming both buy and sell are using market orders, a configuration similar to t ``` Obviously, if only one side is using limit orders, different pricing combinations can be used. ---8<-- "includes/pairlists.md" ---8<-- "includes/protections.md" ## Using Dry-run mode diff --git a/docs/developer.md b/docs/developer.md index 831d9d2f8..c09e528bf 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -177,7 +177,7 @@ In `VolumePairList`, this implements different methods of sorting, does early va ### Protections -Best read the [Protection documentation](configuration.md#protections) to understand protections. +Best read the [Protection documentation](plugins.md#protections) to understand protections. This Guide is directed towards Developers who want to develop a new protection. No protection should use datetime directly, but use the provided `date_now` variable for date calculations. This preserves the ability to backtest protections. diff --git a/docs/utils.md b/docs/utils.md index 409bcc134..cf7d5f1d1 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -391,7 +391,7 @@ $ freqtrade list-markets --exchange kraken --all ## Test pairlist -Use the `test-pairlist` subcommand to test the configuration of [dynamic pairlists](configuration.md#pairlists). +Use the `test-pairlist` subcommand to test the configuration of [dynamic pairlists](plugins.md#pairlists). Requires a configuration with specified `pairlists` attribute. Can be used to generate static pairlists to be used during backtesting / hyperopt. @@ -415,7 +415,7 @@ optional arguments: ### Examples -Show whitelist when using a [dynamic pairlist](configuration.md#pairlists). +Show whitelist when using a [dynamic pairlist](plugins.md#pairlists). ``` freqtrade test-pairlist --config config.json --quote USDT BTC From 43986d3f739d268f4cba0464e04abaca522b8961 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Feb 2021 20:26:01 +0100 Subject: [PATCH 309/563] Move Pricing to subpage --- docs/configuration.md | 128 +-------------------------------------- docs/includes/pricing.md | 127 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 127 deletions(-) create mode 100644 docs/includes/pricing.md diff --git a/docs/configuration.md b/docs/configuration.md index e5eac973f..c6c40319d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -448,133 +448,7 @@ The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT" ``` -## Prices used for orders - -Prices for regular orders can be controlled via the parameter structures `bid_strategy` for buying and `ask_strategy` for selling. -Prices are always retrieved right before an order is placed, either by querying the exchange tickers or by using the orderbook data. - -!!! Note - Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details. - -!!! Warning "Using market orders" - Please read the section [Market order pricing](#market-order-pricing) section when using market orders. - -### Buy price - -#### Check depth of market - -When check depth of market is enabled (`bid_strategy.check_depth_of_market.enabled=True`), the buy signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side. - -Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `bid_strategy.check_depth_of_market.bids_to_ask_delta` parameter. The buy order is only executed if the orderbook delta is greater than or equal to the configured delta value. - -!!! Note - A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side). - -#### Buy price side - -The configuration setting `bid_strategy.price_side` defines the side of the spread the bot looks for when buying. - -The following displays an orderbook. - -``` explanation -... -103 -102 -101 # ask --------------Current spread -99 # bid -98 -97 -... -``` - -If `bid_strategy.price_side` is set to `"bid"`, then the bot will use 99 as buying price. -In line with that, if `bid_strategy.price_side` is set to `"ask"`, then the bot will use 101 as buying price. - -Using `ask` price often guarantees quicker filled orders, but the bot can also end up paying more than what would have been necessary. -Taker fees instead of maker fees will most likely apply even when using limit buy orders. -Also, prices at the "ask" side of the spread are higher than prices at the "bid" side in the orderbook, so the order behaves similar to a market order (however with a maximum price). - -#### Buy price with Orderbook enabled - -When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on. - -#### Buy price without Orderbook enabled - -The following section uses `side` as the configured `bid_strategy.price_side`. - -When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. - -The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price. - -### Sell price - -#### Sell price side - -The configuration setting `ask_strategy.price_side` defines the side of the spread the bot looks for when selling. - -The following displays an orderbook: - -``` explanation -... -103 -102 -101 # ask --------------Current spread -99 # bid -98 -97 -... -``` - -If `ask_strategy.price_side` is set to `"ask"`, then the bot will use 101 as selling price. -In line with that, if `ask_strategy.price_side` is set to `"bid"`, then the bot will use 99 as selling price. - -#### Sell price with Orderbook enabled - -When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the configured orderbook side are validated for a profitable sell-possibility based on the strategy configuration (`minimal_roi` conditions) and the sell order is placed at the first profitable spot. - -!!! Note - Using `order_book_max` higher than `order_book_min` only makes sense when ask_strategy.price_side is set to `"ask"`. - -The idea here is to place the sell order early, to be ahead in the queue. - -A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number. - -!!! Warning "Order_book_max > 1 - increased risks for stoplosses!" - Using `ask_strategy.order_book_max` higher than 1 will increase the risk the stoploss on exchange is cancelled too early, since an eventual [stoploss on exchange](#understand-order_types) will be cancelled as soon as the order is placed. - Also, the sell order will remain on the exchange for `unfilledtimeout.sell` (or until it's filled) - which can lead to missed stoplosses (with or without using stoploss on exchange). - -!!! Warning "Order_book_max > 1 in dry-run" - Using `ask_strategy.order_book_max` higher than 1 will result in improper dry-run results (significantly better than real orders executed on exchange), since dry-run assumes orders to be filled almost instantly. - It is therefore advised to not use this setting for dry-runs. - -#### Sell price without Orderbook enabled - -When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. - -### Market order pricing - -When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection. -Assuming both buy and sell are using market orders, a configuration similar to the following might be used - -``` jsonc - "order_types": { - "buy": "market", - "sell": "market" - // ... - }, - "bid_strategy": { - "price_side": "ask", - // ... - }, - "ask_strategy":{ - "price_side": "bid", - // ... - }, -``` - -Obviously, if only one side is using limit orders, different pricing combinations can be used. +--8<-- "includes/pricing.md" ## Using Dry-run mode diff --git a/docs/includes/pricing.md b/docs/includes/pricing.md new file mode 100644 index 000000000..d8a72cc58 --- /dev/null +++ b/docs/includes/pricing.md @@ -0,0 +1,127 @@ +## Prices used for orders + +Prices for regular orders can be controlled via the parameter structures `bid_strategy` for buying and `ask_strategy` for selling. +Prices are always retrieved right before an order is placed, either by querying the exchange tickers or by using the orderbook data. + +!!! Note + Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details. + +!!! Warning "Using market orders" + Please read the section [Market order pricing](#market-order-pricing) section when using market orders. + +### Buy price + +#### Check depth of market + +When check depth of market is enabled (`bid_strategy.check_depth_of_market.enabled=True`), the buy signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side. + +Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `bid_strategy.check_depth_of_market.bids_to_ask_delta` parameter. The buy order is only executed if the orderbook delta is greater than or equal to the configured delta value. + +!!! Note + A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side). + +#### Buy price side + +The configuration setting `bid_strategy.price_side` defines the side of the spread the bot looks for when buying. + +The following displays an orderbook. + +``` explanation +... +103 +102 +101 # ask +-------------Current spread +99 # bid +98 +97 +... +``` + +If `bid_strategy.price_side` is set to `"bid"`, then the bot will use 99 as buying price. +In line with that, if `bid_strategy.price_side` is set to `"ask"`, then the bot will use 101 as buying price. + +Using `ask` price often guarantees quicker filled orders, but the bot can also end up paying more than what would have been necessary. +Taker fees instead of maker fees will most likely apply even when using limit buy orders. +Also, prices at the "ask" side of the spread are higher than prices at the "bid" side in the orderbook, so the order behaves similar to a market order (however with a maximum price). + +#### Buy price with Orderbook enabled + +When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on. + +#### Buy price without Orderbook enabled + +The following section uses `side` as the configured `bid_strategy.price_side`. + +When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. + +The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price. + +### Sell price + +#### Sell price side + +The configuration setting `ask_strategy.price_side` defines the side of the spread the bot looks for when selling. + +The following displays an orderbook: + +``` explanation +... +103 +102 +101 # ask +-------------Current spread +99 # bid +98 +97 +... +``` + +If `ask_strategy.price_side` is set to `"ask"`, then the bot will use 101 as selling price. +In line with that, if `ask_strategy.price_side` is set to `"bid"`, then the bot will use 99 as selling price. + +#### Sell price with Orderbook enabled + +When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the configured orderbook side are validated for a profitable sell-possibility based on the strategy configuration (`minimal_roi` conditions) and the sell order is placed at the first profitable spot. + +!!! Note + Using `order_book_max` higher than `order_book_min` only makes sense when ask_strategy.price_side is set to `"ask"`. + +The idea here is to place the sell order early, to be ahead in the queue. + +A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number. + +!!! Warning "Order_book_max > 1 - increased risks for stoplosses!" + Using `ask_strategy.order_book_max` higher than 1 will increase the risk the stoploss on exchange is cancelled too early, since an eventual [stoploss on exchange](#understand-order_types) will be cancelled as soon as the order is placed. + Also, the sell order will remain on the exchange for `unfilledtimeout.sell` (or until it's filled) - which can lead to missed stoplosses (with or without using stoploss on exchange). + +!!! Warning "Order_book_max > 1 in dry-run" + Using `ask_strategy.order_book_max` higher than 1 will result in improper dry-run results (significantly better than real orders executed on exchange), since dry-run assumes orders to be filled almost instantly. + It is therefore advised to not use this setting for dry-runs. + +#### Sell price without Orderbook enabled + +When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. + +### Market order pricing + +When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection. +Assuming both buy and sell are using market orders, a configuration similar to the following might be used + +``` jsonc + "order_types": { + "buy": "market", + "sell": "market" + // ... + }, + "bid_strategy": { + "price_side": "ask", + // ... + }, + "ask_strategy":{ + "price_side": "bid", + // ... + }, +``` + +Obviously, if only one side is using limit orders, different pricing combinations can be used. From 12bcbf437486599350c3a6a9a46d4acd4c887399 Mon Sep 17 00:00:00 2001 From: mobrine1 <33225846+mobrine1@users.noreply.github.com> Date: Tue, 2 Feb 2021 15:40:33 -0500 Subject: [PATCH 310/563] #4289 printing json output Adding --json flag to print json output --- scripts/rest_client.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 2232b8421..6a3b2126b 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -331,6 +331,13 @@ def add_arguments(): default=[] ) + parser.add_argument('--json', + help='Output data in json form rather than a python dict', + dest='json', + action='store_true', + default=False + ) + args = parser.parse_args() return vars(args) @@ -379,8 +386,11 @@ def main(args): print_commands() return - print(getattr(client, command)(*args["command_arguments"])) - + output = getattr(client, command)(*args["command_arguments"]) + if args['json']: + print(json.dumps(output)) + else: + print(output) if __name__ == "__main__": args = add_arguments() From 56569690d978b03ee23f3b7d24d9ceb04a8681fc Mon Sep 17 00:00:00 2001 From: mobrine1 <33225846+mobrine1@users.noreply.github.com> Date: Tue, 2 Feb 2021 15:59:48 -0500 Subject: [PATCH 311/563] Update rest_client.py --- scripts/rest_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 6a3b2126b..b214acda3 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -392,6 +392,7 @@ def main(args): else: print(output) + if __name__ == "__main__": args = add_arguments() main(args) From 06b56544a8f51e4e87646acabe14ef166c0638b8 Mon Sep 17 00:00:00 2001 From: mobrine1 <33225846+mobrine1@users.noreply.github.com> Date: Wed, 3 Feb 2021 03:27:54 -0500 Subject: [PATCH 312/563] printing json by default now --- scripts/rest_client.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index b214acda3..b6e66cfa4 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -331,13 +331,6 @@ def add_arguments(): default=[] ) - parser.add_argument('--json', - help='Output data in json form rather than a python dict', - dest='json', - action='store_true', - default=False - ) - args = parser.parse_args() return vars(args) @@ -386,11 +379,7 @@ def main(args): print_commands() return - output = getattr(client, command)(*args["command_arguments"]) - if args['json']: - print(json.dumps(output)) - else: - print(output) + print(json.dumps(getattr(client, command)(*args["command_arguments"]))) if __name__ == "__main__": From f36c61e32fe09c4e7dda2fcb848fb6fe11de2265 Mon Sep 17 00:00:00 2001 From: Alberto del Barrio Date: Wed, 3 Feb 2021 18:11:49 +0100 Subject: [PATCH 313/563] Fix documentation links pointing to pairlists --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c6c40319d..25ae1dd31 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -82,8 +82,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String -| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List -| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List +| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List +| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List | `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict From b8cb39462c72b049120a91795d8b0b7797e748a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Feb 2021 20:00:33 +0100 Subject: [PATCH 314/563] Move get_trade_stake_amount to wallets this way it can be easier used by other functions --- freqtrade/freqtradebot.py | 82 ++------------------------------------ freqtrade/rpc/rpc.py | 3 +- freqtrade/wallets.py | 78 ++++++++++++++++++++++++++++++++++++ tests/test_freqtradebot.py | 28 ++++++++----- tests/test_integration.py | 6 ++- 5 files changed, 104 insertions(+), 93 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2656daab5..fde85e94a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -233,7 +233,7 @@ class FreqtradeBot(LoggingMixin): _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) return _whitelist - def get_free_open_trades(self): + def get_free_open_trades(self) -> int: """ Return the number of free open trades slots or 0 if max number of open trades reached @@ -439,83 +439,6 @@ class FreqtradeBot(LoggingMixin): return used_rate - def get_trade_stake_amount(self, pair: str) -> float: - """ - Calculate stake amount for the trade - :return: float: Stake amount - :raise: DependencyException if the available stake amount is too low - """ - stake_amount: float - # Ensure wallets are uptodate. - self.wallets.update() - - if self.edge: - stake_amount = self.edge.stake_amount( - pair, - self.wallets.get_free(self.config['stake_currency']), - self.wallets.get_total(self.config['stake_currency']), - Trade.total_open_trades_stakes() - ) - else: - stake_amount = self.config['stake_amount'] - if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: - stake_amount = self._calculate_unlimited_stake_amount() - - return self._check_available_stake_amount(stake_amount) - - def _get_available_stake_amount(self) -> float: - """ - Return the total currently available balance in stake currency, - respecting tradable_balance_ratio. - Calculated as - + free amount ) * tradable_balance_ratio - - """ - val_tied_up = Trade.total_open_trades_stakes() - - # Ensure % is used from the overall balance - # Otherwise we'd risk lowering stakes with each open trade. - # (tied up + current free) * ratio) - tied up - available_amount = ((val_tied_up + self.wallets.get_free(self.config['stake_currency'])) * - self.config['tradable_balance_ratio']) - val_tied_up - return available_amount - - def _calculate_unlimited_stake_amount(self) -> float: - """ - Calculate stake amount for "unlimited" stake amount - :return: 0 if max number of trades reached, else stake_amount to use. - """ - free_open_trades = self.get_free_open_trades() - if not free_open_trades: - return 0 - - available_amount = self._get_available_stake_amount() - - return available_amount / free_open_trades - - def _check_available_stake_amount(self, stake_amount: float) -> float: - """ - Check if stake amount can be fulfilled with the available balance - for the stake currency - :return: float: Stake amount - """ - available_amount = self._get_available_stake_amount() - - if self.config['amend_last_stake_amount']: - # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio - # Otherwise the remaining amount is too low to trade. - if available_amount > (stake_amount * self.config['last_stake_amount_min_ratio']): - stake_amount = min(stake_amount, available_amount) - else: - stake_amount = 0 - - if available_amount < stake_amount: - raise DependencyException( - f"Available balance ({available_amount} {self.config['stake_currency']}) is " - f"lower than stake amount ({stake_amount} {self.config['stake_currency']})" - ) - - return stake_amount - def create_trade(self, pair: str) -> bool: """ Check the implemented trading strategy for buy signals. @@ -549,7 +472,8 @@ class FreqtradeBot(LoggingMixin): (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: - stake_amount = self.get_trade_stake_amount(pair) + stake_amount = self.wallets.get_trade_stake_amount(pair, self.get_free_open_trades(), + self.edge) if not stake_amount: logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 491d1cde6..379fdcefa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -590,7 +590,8 @@ class RPC: raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount - stakeamount = self._freqtrade.get_trade_stake_amount(pair) + stakeamount = self._freqtrade.wallets.get_trade_stake_amount( + pair, self._freqtrade.get_free_open_trades()) # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 3680dd416..d7dcfd487 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -7,6 +7,8 @@ from typing import Any, Dict, NamedTuple import arrow +from freqtrade.constants import UNLIMITED_STAKE_AMOUNT +from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange from freqtrade.persistence import Trade @@ -118,3 +120,79 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: return self._wallets + + def _get_available_stake_amount(self) -> float: + """ + Return the total currently available balance in stake currency, + respecting tradable_balance_ratio. + Calculated as + ( + free amount ) * tradable_balance_ratio - + """ + val_tied_up = Trade.total_open_trades_stakes() + + # Ensure % is used from the overall balance + # Otherwise we'd risk lowering stakes with each open trade. + # (tied up + current free) * ratio) - tied up + available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) * + self._config['tradable_balance_ratio']) - val_tied_up + return available_amount + + def _calculate_unlimited_stake_amount(self, free_open_trades: int) -> float: + """ + Calculate stake amount for "unlimited" stake amount + :return: 0 if max number of trades reached, else stake_amount to use. + """ + if not free_open_trades: + return 0 + + available_amount = self._get_available_stake_amount() + + return available_amount / free_open_trades + + def _check_available_stake_amount(self, stake_amount: float) -> float: + """ + Check if stake amount can be fulfilled with the available balance + for the stake currency + :return: float: Stake amount + """ + available_amount = self._get_available_stake_amount() + + if self._config['amend_last_stake_amount']: + # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio + # Otherwise the remaining amount is too low to trade. + if available_amount > (stake_amount * self._config['last_stake_amount_min_ratio']): + stake_amount = min(stake_amount, available_amount) + else: + stake_amount = 0 + + if available_amount < stake_amount: + raise DependencyException( + f"Available balance ({available_amount} {self._config['stake_currency']}) is " + f"lower than stake amount ({stake_amount} {self._config['stake_currency']})" + ) + + return stake_amount + + def get_trade_stake_amount(self, pair: str, free_open_trades: int, edge=None) -> float: + """ + Calculate stake amount for the trade + :return: float: Stake amount + :raise: DependencyException if the available stake amount is too low + """ + stake_amount: float + # Ensure wallets are uptodate. + self.update() + + if edge: + stake_amount = edge.stake_amount( + pair, + self.get_free(self._config['stake_currency']), + self.get_total(self._config['stake_currency']), + Trade.total_open_trades_stakes() + ) + else: + stake_amount = self._config['stake_amount'] + if stake_amount == UNLIMITED_STAKE_AMOUNT: + stake_amount = self._calculate_unlimited_stake_amount(free_open_trades) + + return self._check_available_stake_amount(stake_amount) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index abb91d66b..6cb126ae1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -158,7 +158,8 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: freqtrade = FreqtradeBot(default_conf) - result = freqtrade.get_trade_stake_amount('ETH/BTC') + result = freqtrade.wallets.get_trade_stake_amount( + 'ETH/BTC', freqtrade.get_free_open_trades()) assert result == default_conf['stake_amount'] @@ -194,12 +195,14 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b if expected[i] is not None: limit_buy_order_open['id'] = str(i) - result = freqtrade.get_trade_stake_amount('ETH/BTC') + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', + freqtrade.get_free_open_trades()) assert pytest.approx(result) == expected[i] freqtrade.execute_buy('ETH/BTC', result) else: with pytest.raises(DependencyException): - freqtrade.get_trade_stake_amount('ETH/BTC') + freqtrade.wallets.get_trade_stake_amount('ETH/BTC', + freqtrade.get_free_open_trades()) def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: @@ -210,7 +213,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: patch_get_signal(freqtrade) with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.get_trade_stake_amount('ETH/BTC') + freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) @pytest.mark.parametrize("balance_ratio,result1", [ @@ -239,25 +242,25 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r patch_get_signal(freqtrade) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.get_trade_stake_amount('ETH/BTC') + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/BTC', result) - result = freqtrade.get_trade_stake_amount('LTC/BTC') + result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade.get_trade_stake_amount('XRP/BTC') + result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) assert result == 0 # set max_open_trades = None, so do not trade conf['max_open_trades'] = 0 freqtrade = FreqtradeBot(conf) - result = freqtrade.get_trade_stake_amount('NEO/BTC') + result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) assert result == 0 @@ -283,8 +286,10 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: edge_conf['dry_run_wallet'] = 999.9 freqtrade = FreqtradeBot(edge_conf) - assert freqtrade.get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 - assert freqtrade.get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21 + assert freqtrade.wallets.get_trade_stake_amount( + 'NEO/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 + assert freqtrade.wallets.get_trade_stake_amount( + 'LTC/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: @@ -500,7 +505,8 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade) assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.get_trade_stake_amount('ETH/BTC') == 0 + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades(), + freqtrade.edge) == 0 def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, diff --git a/tests/test_integration.py b/tests/test_integration.py index 9695977ac..8e3bd251a 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -178,7 +178,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc trades = Trade.query.all() assert len(trades) == 4 - assert freqtrade.get_trade_stake_amount('XRP/BTC') == result1 + assert freqtrade.wallets.get_trade_stake_amount( + 'XRP/BTC', freqtrade.get_free_open_trades()) == result1 rpc._rpc_forcebuy('TKN/BTC', None) @@ -199,7 +200,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc # One trade sold assert len(trades) == 4 # stake-amount should now be reduced, since one trade was sold at a loss. - assert freqtrade.get_trade_stake_amount('XRP/BTC') < result1 + assert freqtrade.wallets.get_trade_stake_amount( + 'XRP/BTC', freqtrade.get_free_open_trades()) < result1 # Validate that balance of sold trade is not in dry-run balances anymore. bals2 = freqtrade.wallets.get_all_balances() assert bals != bals2 From e8e5acc2e2ffcde04b75e27cfc4903e3583d2eff Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Feb 2021 20:15:08 +0100 Subject: [PATCH 315/563] Fix import in strategy template --- freqtrade/templates/base_strategy.py.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 4a1b43e36..dd6b773e1 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -5,7 +5,7 @@ import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy # -------------------------------- # Add your lib to import here From 99b2214d1f85b4032d1caf2b5b273c1b3e4c52a6 Mon Sep 17 00:00:00 2001 From: raoulus Date: Thu, 4 Feb 2021 15:27:18 +0100 Subject: [PATCH 316/563] setting resize_keyboard=True for slightly smaller Telegram buttons --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 99f9a8a91..0f7005639 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -910,7 +910,7 @@ class Telegram(RPCHandler): :param parse_mode: telegram parse mode :return: None """ - reply_markup = ReplyKeyboardMarkup(self._keyboard) + reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) try: try: self._updater.bot.send_message( From 5cd8745997ed2300fc750408d4655ca1345963f4 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 4 Feb 2021 16:26:03 +0100 Subject: [PATCH 317/563] Update README.md Typo playing -> paying --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51a3409ea..bb136d7f2 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Please find the complete documentation on our [website](https://www.freqtrade.io - [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux. - [x] **Persistence**: Persistence is achieved through sqlite. -- [x] **Dry-run**: Run the bot without playing money. +- [x] **Dry-run**: Run the bot without paying money. - [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/). From 5165357f40c780358271f7ae6d31aa14f93e7feb Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 4 Feb 2021 19:36:04 +0100 Subject: [PATCH 318/563] Update data-download.md Fix wrong path Add section about fixing wrong docker permission, if user_data is created by docker, it's permission are set to `root` --- docs/data-download.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index 4c7376933..cad39ac41 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -264,7 +264,19 @@ If you are using Binance for example: ```bash mkdir -p user_data/data/binance -cp freqtrade/tests/testdata/pairs.json user_data/data/binance +cp tests/testdata/pairs.json user_data/data/binance +``` + +if you your configuration directory `user_data` was made by docker, you may get an error: + +``` +cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied +``` + +so you will need to fix your permission by doing: + +``` +chown -R $UID:$GID user_data ``` The format of the `pairs.json` file is a simple json list. From 428d2af312fea93e2e01da0feed6d808269c2723 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 4 Feb 2021 19:39:25 +0100 Subject: [PATCH 319/563] add `sudo` to `chown` that was the whole point d'oh --- docs/data-download.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index cad39ac41..1e183d04b 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -276,7 +276,7 @@ cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission d so you will need to fix your permission by doing: ``` -chown -R $UID:$GID user_data +sudo chown -R $UID:$GID user_data ``` The format of the `pairs.json` file is a simple json list. From 1310a7b5473a53be10784451473851de34b434a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Feb 2021 19:58:44 +0100 Subject: [PATCH 320/563] Fix bug with wrong conversion for BTCST/BTC This can happen if a pair starts with the stake-currency closes #4307 --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 491d1cde6..b17183be2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -451,7 +451,7 @@ class RPC: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) rate = tickers.get(pair, {}).get('bid', None) if rate: - if pair.startswith(stake_currency): + if pair.startswith(stake_currency) and not pair.endswith(stake_currency): rate = 1.0 / rate est_stake = rate * balance.total except (ExchangeError): From a816fb12454cd62a306d80f493af8abcf89cb699 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Fri, 5 Feb 2021 12:43:19 +0100 Subject: [PATCH 321/563] chore(lint): lint binance example config --- config_binance.json.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config_binance.json.example b/config_binance.json.example index 83c9748d7..4fa615d6d 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -12,15 +12,15 @@ "sell": 30 }, "bid_strategy": { - "use_order_book": false, "ask_last_balance": 0.0, + "use_order_book": false, "order_book_top": 1, "check_depth_of_market": { "enabled": false, "bids_to_ask_delta": 1 } }, - "ask_strategy":{ + "ask_strategy": { "use_order_book": false, "order_book_min": 1, "order_book_max": 1, From 0806202d47cd63674a64e6069afe08c1e9cfdaa4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Feb 2021 20:02:55 +0100 Subject: [PATCH 322/563] ccxt version bump to 1.41.62 --- requirements.txt | 2 +- tests/exchange/test_ccxt_compat.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5114f397f..299c07734 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.0 pandas==1.2.1 -ccxt==1.41.35 +ccxt==1.41.62 aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 0c8b7bdcf..8e1d074aa 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -18,7 +18,7 @@ EXCHANGES = { 'bittrex': { 'pair': 'BTC/USDT', 'hasQuoteVolume': False, - 'timeframe': '5m', + 'timeframe': '1h', }, 'binance': { 'pair': 'BTC/USDT', @@ -120,7 +120,9 @@ class TestCCXTExchange(): ohlcv = exchange.refresh_latest_ohlcv([pair_tf]) assert isinstance(ohlcv, dict) assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) - assert len(exchange.klines(pair_tf)) > 200 + # assert len(exchange.klines(pair_tf)) > 200 + # Assume 90% uptime ... + assert len(exchange.klines(pair_tf)) > exchange._ohlcv_candle_limit * 0.90 # TODO: tests fetch_trades (?) From 86a97988c011de79be5e74f3ec408c31b11f7c37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Feb 2021 20:09:13 +0100 Subject: [PATCH 323/563] Improve wording --- docs/data-download.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 1e183d04b..04f444a8b 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -267,13 +267,13 @@ mkdir -p user_data/data/binance cp tests/testdata/pairs.json user_data/data/binance ``` -if you your configuration directory `user_data` was made by docker, you may get an error: +If you your configuration directory `user_data` was made by docker, you may get the following error: ``` cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied ``` -so you will need to fix your permission by doing: +You can fix the permissions of your user-data directory as follows: ``` sudo chown -R $UID:$GID user_data From aec22c5c3d53b6a21f677c4e62427df78e073c76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Feb 2021 20:17:38 +0100 Subject: [PATCH 324/563] introduce skip_open_order_update parameter skips startup-open-order-update closes #4128 --- docs/configuration.md | 1 + freqtrade/freqtradebot.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 25ae1dd31..00d2830e4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -89,6 +89,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer | `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`
**Datatype:** Boolean | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fde85e94a..71b6c7dc0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -246,7 +246,7 @@ class FreqtradeBot(LoggingMixin): Updates open orders based on order list kept in the database. Mainly updates the state of orders - but may also close trades """ - if self.config['dry_run']: + if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False): # Updating open orders in dry-run does not make sense and will fail. return From 0a43988f3f9c61e82680f4166a04047acac3b626 Mon Sep 17 00:00:00 2001 From: Edvinas Selskas Date: Sat, 6 Feb 2021 03:02:37 +0000 Subject: [PATCH 325/563] Fix sample strategy documentation link Noticed that the current link is dead. I think this would be the most appropriate link in this case. --- freqtrade/templates/sample_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index b3f9fef07..b1a1bb0ea 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -17,7 +17,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class SampleStrategy(IStrategy): """ This is a sample strategy to inspire you. - More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/strategy-customization.md You can: :return: a Dataframe with all mandatory indicators for the strategies From bc8fda8d6345086fa7bb827fc894fcfad61b20ef Mon Sep 17 00:00:00 2001 From: Edvinas Selskas Date: Sat, 6 Feb 2021 03:13:53 +0000 Subject: [PATCH 326/563] Update sample_strategy.py Fix test --- freqtrade/templates/sample_strategy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index b1a1bb0ea..6dd5a01ce 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -17,7 +17,8 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class SampleStrategy(IStrategy): """ This is a sample strategy to inspire you. - More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/strategy-customization.md + More information in: + https://github.com/freqtrade/freqtrade/blob/develop/docs/strategy-customization.md You can: :return: a Dataframe with all mandatory indicators for the strategies From d5cf837c0f5a6e0c2e0950d49721249587e27a82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Feb 2021 09:23:10 +0100 Subject: [PATCH 327/563] Parse regular cancel_order call to update orders table --- freqtrade/freqtradebot.py | 5 ++++- tests/test_freqtradebot.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 71b6c7dc0..a6eb75d5b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1070,7 +1070,9 @@ class FreqtradeBot(LoggingMixin): if not self.exchange.check_order_canceled_empty(order): try: # if trade is not partially completed, just delete the order - self.exchange.cancel_order(trade.open_order_id, trade.pair) + co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, + trade.amount) + trade.update_order(co) except InvalidOrderException: logger.exception(f"Could not cancel sell order {trade.open_order_id}") return 'error cancelling order' @@ -1078,6 +1080,7 @@ class FreqtradeBot(LoggingMixin): else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] logger.info('Sell order %s for %s.', reason, trade) + trade.update_order(order) trade.close_rate = None trade.close_rate_requested = None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6cb126ae1..3bd2f5607 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2609,7 +2609,7 @@ def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch( - 'freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) + 'freqtrade.exchange.Exchange.cancel_order_with_result', side_effect=InvalidOrderException()) freqtrade = FreqtradeBot(default_conf) From 694f55c0a53174d430ed5f7e2e8b20ab7c3b3e74 Mon Sep 17 00:00:00 2001 From: Edvinas Selskas Date: Sat, 6 Feb 2021 14:43:50 +0000 Subject: [PATCH 328/563] Use suggested link --- freqtrade/templates/sample_strategy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 6dd5a01ce..db1ba48b8 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -17,8 +17,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class SampleStrategy(IStrategy): """ This is a sample strategy to inspire you. - More information in: - https://github.com/freqtrade/freqtrade/blob/develop/docs/strategy-customization.md + More information in https://www.freqtrade.io/en/latest/strategy-customization/ You can: :return: a Dataframe with all mandatory indicators for the strategies From d1bb46bed015c7d6364c2aab0a84aba9c5007527 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 05:27:42 +0000 Subject: [PATCH 329/563] Bump py-find-1st from 1.1.4 to 1.1.5 Bumps [py-find-1st](https://github.com/roebel/py_find_1st) from 1.1.4 to 1.1.5. - [Release notes](https://github.com/roebel/py_find_1st/releases) - [Commits](https://github.com/roebel/py_find_1st/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 299c07734..2448afe93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ tables==3.6.1 blosc==1.10.2 # find first, C search in arrays -py_find_1st==1.1.4 +py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.0 From dd7f9181c53b91832b98c549dea2deafdbff350a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 05:27:44 +0000 Subject: [PATCH 330/563] Bump numpy from 1.20.0 to 1.20.1 Bumps [numpy](https://github.com/numpy/numpy) from 1.20.0 to 1.20.1. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.20.0...v1.20.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 299c07734..bdaca4a44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.20.0 +numpy==1.20.1 pandas==1.2.1 ccxt==1.41.62 From 676cd7bb55d85d8829c1adefce83871e98351896 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 05:27:45 +0000 Subject: [PATCH 331/563] Bump sqlalchemy from 1.3.22 to 1.3.23 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.22 to 1.3.23. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 299c07734..c9559e384 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.2.1 ccxt==1.41.62 aiohttp==3.7.3 -SQLAlchemy==1.3.22 +SQLAlchemy==1.3.23 python-telegram-bot==13.1 arrow==0.17.0 cachetools==4.2.1 From 22d447b3f51a1f21632e329e991ebcbdc7f17918 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 05:27:46 +0000 Subject: [PATCH 332/563] Bump mkdocs-material from 6.2.7 to 6.2.8 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.2.7 to 6.2.8. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.2.7...6.2.8) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 85bd72323..94b2fca39 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.2.7 +mkdocs-material==6.2.8 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 12168cbf01edaa6c11ea5e427a52180a34f549ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 08:26:18 +0000 Subject: [PATCH 333/563] Bump ccxt from 1.41.62 to 1.41.70 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.41.62 to 1.41.70. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.41.62...1.41.70) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a9e6e5da..f496df00d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.1 -ccxt==1.41.62 +ccxt==1.41.70 aiohttp==3.7.3 SQLAlchemy==1.3.23 python-telegram-bot==13.1 From c412f8df62265131e4a302331ea1de2e5d922d2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 08:28:11 +0000 Subject: [PATCH 334/563] Bump python-telegram-bot from 13.1 to 13.2 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.1 to 13.2. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.1...v13.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a9e6e5da..c1cc036a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.2.1 ccxt==1.41.62 aiohttp==3.7.3 SQLAlchemy==1.3.23 -python-telegram-bot==13.1 +python-telegram-bot==13.2 arrow==0.17.0 cachetools==4.2.1 requests==2.25.1 From de727645ab7774abdc54c531370baa051aeb3635 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Feb 2021 19:21:33 +0100 Subject: [PATCH 335/563] FIx random test failure if certain files exist --- tests/test_configuration.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index bebbc1508..94c3e24f6 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -743,18 +743,18 @@ def test_set_loggers_journald_importerror(mocker, import_fails): logger.handlers = orig_handlers -def test_set_logfile(default_conf, mocker): +def test_set_logfile(default_conf, mocker, tmpdir): patched_configuration_load_config_file(mocker, default_conf) - + f = Path(tmpdir / "test_file.log") + assert not f.is_file() arglist = [ - 'trade', '--logfile', 'test_file.log', + 'trade', '--logfile', str(f), ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() - assert validated_conf['logfile'] == "test_file.log" - f = Path("test_file.log") + assert validated_conf['logfile'] == str(f) assert f.is_file() try: f.unlink() From c5ab3a80a596769786b12821cafec4ea08108b6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Feb 2021 19:35:22 +0100 Subject: [PATCH 336/563] Check if order is a dict before parsing closes #4331 --- freqtrade/persistence/models.py | 4 ++++ tests/test_persistence.py | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 375709423..dff59819c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -171,6 +171,10 @@ class Order(_DECL_BASE): """ Get all non-closed orders - useful when trying to batch-update orders """ + if not isinstance(order, dict): + logger.warning(f"{order} is not a valid response object.") + return + filtered_orders = [o for o in orders if o.order_id == order.get('id')] if filtered_orders: oobj = filtered_orders[0] diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 9921f541b..d0d29f142 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1074,7 +1074,7 @@ def test_get_best_pair(fee): @pytest.mark.usefixtures("init_persistence") -def test_update_order_from_ccxt(): +def test_update_order_from_ccxt(caplog): # Most basic order return (only has orderid) o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') assert isinstance(o, Order) @@ -1120,6 +1120,14 @@ def test_update_order_from_ccxt(): with pytest.raises(DependencyException, match=r"Order-id's don't match"): o.update_from_ccxt_object(ccxt_order) + message = "aaaa is not a valid response object." + assert not log_has(message, caplog) + Order.update_orders([o], 'aaaa') + assert log_has(message, caplog) + + # Call regular update - shouldn't fail. + Order.update_orders([o], {'id': '1234'}) + @pytest.mark.usefixtures("init_persistence") def test_select_order(fee): From 427d762746980a3a50702b6ea8de1d28325e26c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Feb 2021 19:37:24 +0100 Subject: [PATCH 337/563] Improve tests for cancel_order to be more realistic --- tests/exchange/test_exchange.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cd24e113e..352250fc7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2072,9 +2072,9 @@ def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, cap def test_cancel_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = False api_mock = MagicMock() - api_mock.cancel_order = MagicMock(return_value=123) + api_mock.cancel_order = MagicMock(return_value={'id': '123'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 + assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == {'id': '123'} with pytest.raises(InvalidOrderException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) @@ -2091,9 +2091,9 @@ def test_cancel_order(default_conf, mocker, exchange_name): def test_cancel_stoploss_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = False api_mock = MagicMock() - api_mock.cancel_order = MagicMock(return_value=123) + api_mock.cancel_order = MagicMock(return_value={'id': '123'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == 123 + assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == {'id': '123'} with pytest.raises(InvalidOrderException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) From 7ee149da5d26c7889e4d561bc2154111226dc84c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Feb 2021 20:08:32 +0100 Subject: [PATCH 338/563] Improve plotting errorhandling closes #4327 --- freqtrade/plot/plotting.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f45ba9b25..4325e537e 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -53,7 +53,7 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): data_format=config.get('dataformat_ohlcv', 'json'), ) - if startup_candles: + if startup_candles and data: min_date, max_date = get_timerange(data) logger.info(f"Loading data from {min_date} to {max_date}") timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')), @@ -67,14 +67,16 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): if not filename.is_dir() and not filename.is_file(): logger.warning("Backtest file is missing skipping trades.") no_trades = True - - trades = load_trades( - config['trade_source'], - db_url=config.get('db_url'), - exportfilename=filename, - no_trades=no_trades, - strategy=config.get('strategy'), - ) + try: + trades = load_trades( + config['trade_source'], + db_url=config.get('db_url'), + exportfilename=filename, + no_trades=no_trades, + strategy=config.get('strategy'), + ) + except ValueError as e: + raise OperationalException(e) from e trades = trim_dataframe(trades, timerange, 'open_date') return {"ohlcv": data, From 86fa75b2863eac67edffd20c1fdb7cc853d2d08e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Feb 2021 06:55:36 +0100 Subject: [PATCH 339/563] Pin version of cryptography --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 59ef69f4f..5d03a9c2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,8 @@ numpy==1.20.1 pandas==1.2.1 ccxt==1.41.70 +# Pin cryptography for now due to rust build errors with piwheels +cryptography==3.3.2 aiohttp==3.7.3 SQLAlchemy==1.3.23 python-telegram-bot==13.2 From 3110d2dbb192efe7944b09f68a10410daad39fea Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Feb 2021 20:03:03 +0100 Subject: [PATCH 340/563] Add small test cases --- tests/exchange/test_exchange.py | 3 +++ tests/optimize/test_backtesting.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 352250fc7..f35a84725 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2462,6 +2462,9 @@ def test_timeframe_to_prev_date(): date = datetime.now(tz=timezone.utc) assert timeframe_to_prev_date("5m") < date + # Does not round + time = datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) + assert timeframe_to_prev_date('5m', time) == time def test_timeframe_to_next_date(): diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 5f811e2e5..c8d4338af 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -341,12 +341,14 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') + sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats') mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) default_conf['timeframe'] = '1m' default_conf['datadir'] = testdatadir - default_conf['export'] = None + default_conf['export'] = 'trades' + default_conf['exportfilename'] = 'export.txt' default_conf['timerange'] = '-1510694220' backtesting = Backtesting(default_conf) @@ -361,6 +363,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: assert log_has(line, caplog) assert backtesting.strategy.dp._pairlists is not None assert backtesting.strategy.bot_loop_start.call_count == 1 + assert sbs.call_count == 1 def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: From aa79574c0c60ba68f8b9ee8e5649d8a785010f5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Feb 2021 17:09:31 +0100 Subject: [PATCH 341/563] Position-size should NEVER be over available_capital Part of #4353 --- freqtrade/edge/edge_positioning.py | 3 ++- tests/edge/test_edge.py | 39 ++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index e549a3701..2bdef1c89 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -159,7 +159,8 @@ class Edge: available_capital = (total_capital + capital_in_trade) * self._capital_ratio allowed_capital_at_risk = available_capital * self._allowed_risk max_position_size = abs(allowed_capital_at_risk / stoploss) - position_size = min(max_position_size, free_capital) + # Position size must be below available capital. + position_size = min(min(max_position_size, free_capital), available_capital) if pair in self._cached_pairs: logger.info( 'winrate: %s, expectancy: %s, position size: %s, pair: %s,' diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index f25dad35b..c30bce6a4 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -209,7 +209,7 @@ def test_nonexisting_stoploss(mocker, edge_conf): assert edge.stoploss('N/O') == -0.1 -def test_stake_amount(mocker, edge_conf): +def test_edge_stake_amount(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( @@ -217,20 +217,33 @@ def test_stake_amount(mocker, edge_conf): 'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60), } )) - free = 100 - total = 100 - in_trade = 25 - assert edge.stake_amount('E/F', free, total, in_trade) == 31.25 + assert edge._capital_ratio == 0.5 + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=25) == 31.25 - free = 20 - total = 100 - in_trade = 25 - assert edge.stake_amount('E/F', free, total, in_trade) == 20 + assert edge.stake_amount('E/F', free_capital=20, total_capital=100, + capital_in_trade=25) == 20 - free = 0 - total = 100 - in_trade = 25 - assert edge.stake_amount('E/F', free, total, in_trade) == 0 + assert edge.stake_amount('E/F', free_capital=0, total_capital=100, + capital_in_trade=25) == 0 + + # Test with increased allowed_risk + # Result should be no more than allowed capital + edge._allowed_risk = 0.4 + edge._capital_ratio = 0.5 + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=25) == 62.5 + + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=0) == 50 + + edge._capital_ratio = 1 + # Full capital is available + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=0) == 100 + # Full capital is available + assert edge.stake_amount('E/F', free_capital=0, total_capital=100, + capital_in_trade=0) == 0 def test_nonexisting_stake_amount(mocker, edge_conf): From 843fb204e9aa680b53aa16061815519d74456ca8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Feb 2021 20:21:07 +0100 Subject: [PATCH 342/563] Fix problem with inf values returned from dataframe for api methods --- freqtrade/rpc/api_server/api_v1.py | 2 +- freqtrade/rpc/rpc.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index a2082103b..3588f2196 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -167,7 +167,7 @@ def reload_config(rpc: RPC = Depends(get_rpc)): @router.get('/pair_candles', response_model=PairHistory, tags=['candle data']) -def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc=Depends(get_rpc)): +def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc: RPC = Depends(get_rpc)): return rpc._rpc_analysed_dataframe(pair, timeframe, limit) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 27563f73b..464b341eb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -9,7 +9,7 @@ from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union import arrow -from numpy import NAN, int64, mean +from numpy import NAN, inf, int64, mean from pandas import DataFrame from freqtrade.configuration.timerange import TimeRange @@ -747,6 +747,7 @@ class RPC: sell_mask = (dataframe['sell'] == 1) sell_signals = int(sell_mask.sum()) dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open'] + dataframe = dataframe.replace([inf, -inf], NAN) dataframe = dataframe.replace({NAN: None}) res = { From dd23f6bcbcd569a0857ac0dc1920b24c2065f05e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Feb 2021 20:29:31 +0100 Subject: [PATCH 343/563] Fix type for getting pairs --- freqtrade/rpc/rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 464b341eb..7549c38be 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -776,7 +776,8 @@ class RPC: }) return res - def _rpc_analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]: + def _rpc_analysed_dataframe(self, pair: str, timeframe: str, + limit: Optional[int]) -> Dict[str, Any]: _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe( pair, timeframe) From 072abde9b71b34022e3a5f5c867847ec12a211fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Feb 2021 20:32:41 +0100 Subject: [PATCH 344/563] Introduce round_coin_value to simplify coin rounding --- freqtrade/constants.py | 10 +++++++++ freqtrade/misc.py | 25 +++++++++++++++++++++++ freqtrade/optimize/optimize_reports.py | 11 +++++----- tests/test_misc.py | 28 +++++++++++++++++++++++--- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 69301ca0e..802ddc2b1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -45,6 +45,16 @@ USERPATH_NOTEBOOKS = 'notebooks' TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] + +# Define decimals per coin for outputs +# Only used for outputs. +DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's +DECIMALS_PER_COIN = { + 'BTC': 8, + 'ETH': 5, +} + + # Soure files with destination directories within user-directory USER_DATA_FILES = { 'sample_strategy.py': USERPATH_STRATEGIES, diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 22e14b564..7bbc24056 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -11,10 +11,35 @@ from typing.io import IO import rapidjson +from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN + logger = logging.getLogger(__name__) +def decimals_per_coin(coin: str): + """ + Helper method getting decimal amount for this coin + example usage: f".{decimals_per_coin('USD')}f" + :param coin: Which coin are we printing the price / value for + """ + return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK) + + +def round_coin_value(value: float, coin: str, show_coin_name=True) -> str: + """ + Get price value for this coin + :param value: Value to be printed + :param coin: Which coin are we printing the price / value for + :param show_coin_name: Return string in format: "222.22 USDT" or "222.22" + :return: Formatted / rounded value (with or without coin name) + """ + if show_coin_name: + return f"{value:.{decimals_per_coin(coin)}f} {coin}" + else: + return f"{value:.{decimals_per_coin(coin)}f}" + + def shorten_date(_date: str) -> str: """ Trim the date so it fits on small screens diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 8edfbaf8d..c70a4cd2d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -10,7 +10,7 @@ from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.btanalysis import calculate_market_change, calculate_max_drawdown -from freqtrade.misc import file_dump_json +from freqtrade.misc import file_dump_json, round_coin_value, decimals_per_coin logger = logging.getLogger(__name__) @@ -38,11 +38,12 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) -def _get_line_floatfmt() -> List[str]: +def _get_line_floatfmt(stake_currency: str) -> List[str]: """ Generate floatformat (goes in line with _generate_result_line()) """ - return ['s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd'] + return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f', + '.2f', 'd', 'd', 'd', 'd'] def _get_line_header(first_column: str, stake_currency: str) -> List[str]: @@ -352,7 +353,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st """ headers = _get_line_header('Pair', stake_currency) - floatfmt = _get_line_floatfmt() + floatfmt = _get_line_floatfmt(stake_currency) 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'] @@ -396,7 +397,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: :param all_results: Dict of containing results for all strategies :return: pretty printed table with tabulate as string """ - floatfmt = _get_line_floatfmt() + floatfmt = _get_line_floatfmt(stake_currency) headers = _get_line_header('Strategy', stake_currency) output = [[ diff --git a/tests/test_misc.py b/tests/test_misc.py index 429da135a..e6ba70aee 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,9 +6,31 @@ from unittest.mock import MagicMock import pytest -from freqtrade.misc import (file_dump_json, file_load_json, format_ms_time, pair_to_filename, - plural, render_template, render_template_with_fallback, - safe_value_fallback, safe_value_fallback2, shorten_date) +from freqtrade.misc import (decimals_per_coin, file_dump_json, file_load_json, format_ms_time, + pair_to_filename, plural, render_template, + render_template_with_fallback, round_coin_value, safe_value_fallback, + safe_value_fallback2, shorten_date) + + +def test_decimals_per_coin(): + assert decimals_per_coin('USDT') == 3 + assert decimals_per_coin('EUR') == 3 + assert decimals_per_coin('BTC') == 8 + assert decimals_per_coin('ETH') == 5 + + +def test_round_coin_value(): + assert round_coin_value(222.222222, 'USDT') == '222.222 USDT' + assert round_coin_value(222.2, 'USDT') == '222.200 USDT' + assert round_coin_value(222.12745, 'EUR') == '222.127 EUR' + assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC' + assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH' + + assert round_coin_value(222.222222, 'USDT', False) == '222.222' + assert round_coin_value(222.2, 'USDT', False) == '222.200' + assert round_coin_value(222.12745, 'EUR', False) == '222.127' + assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121' + assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745' def test_shorten_date() -> None: From e7acee79045bfb5aa8cf485d5aa151d4c33fbf30 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Feb 2021 16:05:56 +0100 Subject: [PATCH 345/563] Improve coin value output by rounding coin specific --- freqtrade/optimize/optimize_reports.py | 6 ++-- freqtrade/rpc/telegram.py | 46 ++++++++++++++------------ tests/rpc/test_rpc_telegram.py | 6 ++-- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c70a4cd2d..118253e86 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -10,7 +10,7 @@ from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.btanalysis import calculate_market_change, calculate_max_drawdown -from freqtrade.misc import file_dump_json, round_coin_value, decimals_per_coin +from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value logger = logging.getLogger(__name__) @@ -384,7 +384,9 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren 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_total_pct'], + t['profit_mean_pct'], t['profit_sum_pct'], + round_coin_value(t['profit_total_abs'], stake_currency, False), + t['profit_total_pct'], ] for t in sell_reason_stats] return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0f7005639..a16299e4b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -18,6 +18,7 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.exceptions import OperationalException +from freqtrade.misc import round_coin_value from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType @@ -189,14 +190,14 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 - message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{limit:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Total:* `({stake_amount:.6f} {stake_currency}").format(**msg) + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}\n" + f"*Amount:* `{msg['amount']:.8f}`\n" + f"*Open Rate:* `{msg['limit']:.8f}`\n" + f"*Current Rate:* `{msg['current_rate']:.8f}`\n" + f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") if msg.get('fiat_currency', None): - message += ", {stake_amount_fiat:.3f} {fiat_currency}".format(**msg) + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" message += ")`" elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: @@ -365,7 +366,7 @@ class Telegram(RPCHandler): ) stats_tab = tabulate( [[day['date'], - f"{day['abs_profit']:.8f} {stats['stake_currency']}", + f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}", f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['trade_count']} trades"] for day in stats['data']], headers=[ @@ -415,18 +416,18 @@ class Telegram(RPCHandler): # Message to display if stats['closed_trade_count'] > 0: markdown_msg = ("*ROI:* Closed trades\n" - f"∙ `{profit_closed_coin:.8f} {stake_cur} " + f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} " f"({profit_closed_percent_mean:.2f}%) " f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" - f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n") + f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n") else: markdown_msg = "`No closed trade` \n" markdown_msg += (f"*ROI:* All trades\n" - f"∙ `{profit_all_coin:.8f} {stake_cur} " + f"∙ `{round_coin_value(profit_all_coin, stake_cur)} " f"({profit_all_percent_mean:.2f}%) " f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" - f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" + f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" f"*Total Trade Count:* `{trade_count}`\n" f"*First Trade opened:* `{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}\n`" @@ -494,15 +495,17 @@ class Telegram(RPCHandler): "Starting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" ) - for currency in result['currencies']: - if currency['est_stake'] > 0.0001: - curr_output = ("*{currency}:*\n" - "\t`Available: {free: .8f}`\n" - "\t`Balance: {balance: .8f}`\n" - "\t`Pending: {used: .8f}`\n" - "\t`Est. {stake}: {est_stake: .8f}`\n").format(**currency) + for curr in result['currencies']: + if curr['est_stake'] > 0.0001: + curr_output = ( + f"*{curr['currency']}:*\n" + f"\t`Available: {curr['free']:.8f}`\n" + f"\t`Balance: {curr['balance']:.8f}`\n" + f"\t`Pending: {curr['used']:.8f}`\n" + f"\t`Est. {curr['stake']}: " + f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") else: - curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) + curr_output = "*{currency}:* not showing <1$ amount \n".format(**curr) # Handle overflowing messsage length if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: @@ -512,8 +515,9 @@ class Telegram(RPCHandler): output += curr_output output += ("\n*Estimated Value*:\n" - "\t`{stake}: {total: .8f}`\n" - "\t`{symbol}: {value: .2f}`\n").format(**result) + f"\t`{result['stake']}: {result['total']: .8f}`\n" + f"\t`{result['symbol']}: " + f"{round_coin_value(result['value'], result['symbol'], False)}`\n") self._send_msg(output) except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1c34b6b26..f065bb4c5 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -519,7 +519,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert '*EUR:*' in result assert 'Balance:' in result assert 'Est. BTC:' in result - assert 'BTC: 12.00000000' in result + assert 'BTC: 12.00000000' in result assert '*XRP:* not showing <1$ amount' in result @@ -1205,7 +1205,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ - '*Total:* `(0.001000 BTC, 12.345 USD)`' + '*Total:* `(0.00100000 BTC, 12.345 USD)`' freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} caplog.clear() @@ -1389,7 +1389,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' - '*Total:* `(0.001000 BTC)`') + '*Total:* `(0.00100000 BTC)`') def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: From d4c8be915cc66e6995d383f27cdd2733e7343bca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Feb 2021 16:11:49 +0100 Subject: [PATCH 346/563] Use fstring where possible --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a16299e4b..88019601c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -505,7 +505,7 @@ class Telegram(RPCHandler): f"\t`Est. {curr['stake']}: " f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") else: - curr_output = "*{currency}:* not showing <1$ amount \n".format(**curr) + curr_output = f"*{curr['currency']}:* not showing <1$ amount \n" # Handle overflowing messsage length if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: From 73d91275c4491d452306bf0e0b53530d1406c4e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 07:11:07 +0100 Subject: [PATCH 347/563] Reset sell_order_status if a new sell-order is placed closes #4365 --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a6eb75d5b..73707e4ec 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1183,6 +1183,7 @@ class FreqtradeBot(LoggingMixin): trade.orders.append(order_obj) trade.open_order_id = order['id'] + trade.sell_order_status = '' trade.close_rate_requested = limit trade.sell_reason = sell_reason.value # In case of market sell orders the order can be closed immediately From 6f77ec063e36a11ec55281c41a1a15ee07e72ed9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 07:22:08 +0100 Subject: [PATCH 348/563] Fix cookieError on python<3.8 Only occurs in combination with api-server enabled, due to some hot-fixing starlette does. Since we load starlette at a later point, we need to replicate starlette's behaviour for now, so sameSite cookies don't create a problem. closes #4356 --- freqtrade/exchange/exchange.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c7625b53c..2b47aa7dd 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -3,6 +3,7 @@ Cryptocurrency Exchanges support """ import asyncio +import http import inspect import logging from copy import deepcopy @@ -34,6 +35,12 @@ CcxtModuleType = Any logger = logging.getLogger(__name__) +# Workaround for adding samesite support to pre 3.8 python +# Only applies to python3.7, and only on certain exchanges (kraken) +# Replicates the fix from starlette (which is actually causing this problem) +http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore + + class Exchange: _config: Dict = {} From 10a11bda34f9116944b6f0c75599d4ce5f179c2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 09:41:50 +0100 Subject: [PATCH 349/563] Document bitvavo as community tested closes #4360 --- README.md | 13 +++++++++++-- docs/index.md | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bb136d7f2..7ef0d4ce7 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,21 @@ expect. We strongly recommend you to have coding and Python knowledge. Do not hesitate to read the source code and understand the mechanism of this bot. -## Exchange marketplaces supported +## Supported Exchange marketplaces + +Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange. - [X] [Bittrex](https://bittrex.com/) - [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#blacklists)) - [X] [Kraken](https://kraken.com/) -- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ +- [X] [FTX](https://ftx.com) +- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ + +### Community tested + +Exchanges confirmed working by the community: + +- [X] [Bitvavo](https://bitvavo.com/) ## Documentation diff --git a/docs/index.md b/docs/index.md index b489861f0..db5088707 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,6 +35,22 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python - Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.). - Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md). +## Supported exchange marketplaces + +Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange. + +- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#blacklists)) +- [X] [Bittrex](https://bittrex.com/) +- [X] [FTX](https://ftx.com) +- [X] [Kraken](https://kraken.com/) +- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ + +### Community tested + +Exchanges confirmed working by the community: + +- [X] [Bitvavo](https://bitvavo.com/) + ## Requirements ### Hardware requirements From 7ecf8f8b802318567467f404552c0fafdb1f76aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 10:08:05 +0100 Subject: [PATCH 350/563] Cleanup candle_limit usage --- freqtrade/exchange/exchange.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2b47aa7dd..b11d2f234 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -433,10 +433,10 @@ class Exchange: Checks if required startup_candles is more than ohlcv_candle_limit. Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default. """ - if startup_candles + 5 > self._ft_has['ohlcv_candle_limit']: + if startup_candles + 5 > self.ohlcv_candle_limit: raise OperationalException( f"This strategy requires {startup_candles} candles to start. " - f"{self.name} only provides {self._ft_has['ohlcv_candle_limit']}.") + f"{self.name} only provides {self.ohlcv_candle_limit}.") def exchange_has(self, endpoint: str) -> bool: """ @@ -721,7 +721,7 @@ class Exchange: """ Get candle history using asyncio and returns the list of candles. Handles all async work for this. - Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call. + Async over one pair, assuming we get `self.ohlcv_candle_limit` candles per call. :param pair: Pair to download :param timeframe: Timeframe to get data for :param since_ms: Timestamp in milliseconds to get history from @@ -751,7 +751,7 @@ class Exchange: Download historic ohlcv """ - one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit + one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit logger.debug( "one_call: %s msecs (%s)", one_call, @@ -853,7 +853,7 @@ class Exchange: data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, since=since_ms, - limit=self._ohlcv_candle_limit) + limit=self.ohlcv_candle_limit) # Some exchanges sort OHLCV in ASC order and others in DESC. # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last) @@ -1026,7 +1026,7 @@ class Exchange: """ Get trade history data using asyncio. Handles all async work and returns the list of candles. - Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call. + Async over one pair, assuming we get `self.ohlcv_candle_limit` candles per call. :param pair: Pair to download :param since: Timestamp in milliseconds to get history from :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined. From 5622bb32471e5a85c340a1ddf3c70c0948611538 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 10:29:45 +0100 Subject: [PATCH 351/563] Make candle_limit optionally timeframe dependent --- freqtrade/exchange/exchange.py | 37 +++++++++++-------- freqtrade/plugins/pairlist/AgeFilter.py | 4 +- .../plugins/pairlist/rangestabilityfilter.py | 4 +- tests/exchange/test_ccxt_compat.py | 8 +++- tests/exchange/test_exchange.py | 4 +- 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b11d2f234..d176e489b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -101,7 +101,6 @@ class Exchange: logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has) # Assign this directly for easy access - self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit'] self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle'] self._trades_pagination = self._ft_has['trades_pagination'] @@ -137,7 +136,8 @@ class Exchange: self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_ordertypes(config.get('order_types', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {})) - self.validate_required_startup_candles(config.get('startup_candle_count', 0)) + self.validate_required_startup_candles(config.get('startup_candle_count', 0), + config.get('timeframe')) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( @@ -198,11 +198,6 @@ class Exchange: def timeframes(self) -> List[str]: return list((self._api.timeframes or {}).keys()) - @property - def ohlcv_candle_limit(self) -> int: - """exchange ohlcv candle limit""" - return int(self._ohlcv_candle_limit) - @property def markets(self) -> Dict: """exchange ccxt markets""" @@ -216,6 +211,17 @@ class Exchange: """exchange ccxt precisionMode""" return self._api.precisionMode + def ohlcv_candle_limit(self, timeframe: str) -> int: + """ + Exchange ohlcv candle limit + Uses ohlcv_candle_limit_per_timeframe if the exchange has different limts + per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit + :param timeframe: Timeframe to check + :return: Candle limit as integer + """ + return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get( + timeframe, self._ft_has.get('ohlcv_candle_limit'))) + def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None, pairs_only: bool = False, active_only: bool = False) -> Dict[str, Any]: """ @@ -428,15 +434,16 @@ class Exchange: raise OperationalException( f'Time in force policies are not supported for {self.name} yet.') - def validate_required_startup_candles(self, startup_candles: int) -> None: + def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> None: """ - Checks if required startup_candles is more than ohlcv_candle_limit. + Checks if required startup_candles is more than ohlcv_candle_limit(). Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default. """ - if startup_candles + 5 > self.ohlcv_candle_limit: + candle_limit = self.ohlcv_candle_limit(timeframe) + if startup_candles + 5 > candle_limit: raise OperationalException( f"This strategy requires {startup_candles} candles to start. " - f"{self.name} only provides {self.ohlcv_candle_limit}.") + f"{self.name} only provides {candle_limit} for {timeframe}.") def exchange_has(self, endpoint: str) -> bool: """ @@ -721,7 +728,7 @@ class Exchange: """ Get candle history using asyncio and returns the list of candles. Handles all async work for this. - Async over one pair, assuming we get `self.ohlcv_candle_limit` candles per call. + Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call. :param pair: Pair to download :param timeframe: Timeframe to get data for :param since_ms: Timestamp in milliseconds to get history from @@ -751,7 +758,7 @@ class Exchange: Download historic ohlcv """ - one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit + one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe) logger.debug( "one_call: %s msecs (%s)", one_call, @@ -853,7 +860,7 @@ class Exchange: data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, since=since_ms, - limit=self.ohlcv_candle_limit) + limit=self.ohlcv_candle_limit(timeframe)) # Some exchanges sort OHLCV in ASC order and others in DESC. # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last) @@ -1026,7 +1033,7 @@ class Exchange: """ Get trade history data using asyncio. Handles all async work and returns the list of candles. - Async over one pair, assuming we get `self.ohlcv_candle_limit` candles per call. + Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call. :param pair: Pair to download :param since: Timestamp in milliseconds to get history from :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined. diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index 8c3a5d22f..8a5379ca6 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -30,10 +30,10 @@ class AgeFilter(IPairList): if self._min_days_listed < 1: raise OperationalException("AgeFilter requires min_days_listed to be >= 1") - if self._min_days_listed > exchange.ohlcv_candle_limit: + if self._min_days_listed > exchange.ohlcv_candle_limit('1d'): raise OperationalException("AgeFilter requires min_days_listed to not exceed " "exchange max request size " - f"({exchange.ohlcv_candle_limit})") + f"({exchange.ohlcv_candle_limit('1d')})") @property def needstickers(self) -> bool: diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index f2e84930b..db51a9c77 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -32,10 +32,10 @@ class RangeStabilityFilter(IPairList): if self._days < 1: raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") - if self._days > exchange.ohlcv_candle_limit: + if self._days > exchange.ohlcv_candle_limit('1d'): raise OperationalException("RangeStabilityFilter requires lookback_days to not " "exceed exchange max request size " - f"({exchange.ohlcv_candle_limit})") + f"({exchange.ohlcv_candle_limit('1d')})") @property def needstickers(self) -> bool: diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 8e1d074aa..a64565c28 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -5,11 +5,14 @@ However, these tests should give a good idea to determine if a new exchange is suitable to run with freqtrade. """ +from datetime import datetime, timedelta, timezone +from freqtrade.exchange.exchange import timeframe_to_minutes from pathlib import Path import pytest from freqtrade.resolvers.exchange_resolver import ExchangeResolver +from freqtrade.exchange import timeframe_to_prev_date from tests.conftest import get_default_conf @@ -122,7 +125,10 @@ class TestCCXTExchange(): assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) # assert len(exchange.klines(pair_tf)) > 200 # Assume 90% uptime ... - assert len(exchange.klines(pair_tf)) > exchange._ohlcv_candle_limit * 0.90 + assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(timeframe) * 0.90 + # Check if last-timeframe is within the last 2 intervals + now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) + assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) # TODO: tests fetch_trades (?) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index f35a84725..3bafb2457 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1417,7 +1417,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls - since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 + since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8 ret = exchange.get_historic_ohlcv(pair, "5m", int(( arrow.utcnow().int_timestamp - since) * 1000)) @@ -1473,7 +1473,7 @@ def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name): exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls - since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 + since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8 ret = exchange.get_historic_ohlcv_as_df(pair, "5m", int(( arrow.utcnow().int_timestamp - since) * 1000)) From da89838b5cf4cd0a17cc8286e355b44962e587d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 10:32:55 +0100 Subject: [PATCH 352/563] Set bittrex limits as returned by the exchange closes #4181 --- freqtrade/exchange/bittrex.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index 4318f9cf0..fd7d47668 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -19,5 +19,11 @@ class Bittrex(Exchange): """ _ft_has: Dict = { + "ohlcv_candle_limit_per_timeframe": { + '1m': 1440, + '5m': 288, + '1h': 744, + '1d': 365, + }, "l2_limit_range": [1, 25, 500], } From ffca09bbcbdb91ca65ef966a2e09bd03e96bceb0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 10:38:49 +0100 Subject: [PATCH 353/563] Test ohlcv_candle_limit explicitly --- tests/exchange/test_exchange.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3bafb2457..75db2de26 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2418,6 +2418,19 @@ def test_get_markets_error(default_conf, mocker): ex.get_markets('LTC', 'USDT', True, False) +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_ohlcv_candle_limit(default_conf, mocker, exchange_name): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + timeframes = ('1m', '5m', '1h') + expected = exchange._ft_has['ohlcv_candle_limit'] + for timeframe in timeframes: + if 'ohlcv_candle_limit_per_timeframe' in exchange._ft_has: + expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe] + # This should only run for bittrex + assert exchange_name == 'bittrex' + assert exchange.ohlcv_candle_limit(timeframe) == expected + + def test_timeframe_to_minutes(): assert timeframe_to_minutes("5m") == 5 assert timeframe_to_minutes("10m") == 10 From ee74bc1f524e9b74c0241926a3fd71cdc2faee09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 10:46:59 +0100 Subject: [PATCH 354/563] timeframe is mandatory, no need to use .get() --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_ccxt_compat.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d176e489b..617cd6c26 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -137,7 +137,7 @@ class Exchange: self.validate_ordertypes(config.get('order_types', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0), - config.get('timeframe')) + config.get('timeframe', '')) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index a64565c28..03cb30d62 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -6,13 +6,12 @@ suitable to run with freqtrade. """ from datetime import datetime, timedelta, timezone -from freqtrade.exchange.exchange import timeframe_to_minutes from pathlib import Path import pytest +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from freqtrade.exchange import timeframe_to_prev_date from tests.conftest import get_default_conf From 5c263c7ffd546ec50fc96dbab49ba502bdf0ff33 Mon Sep 17 00:00:00 2001 From: Florian Reitmeir Date: Thu, 24 Dec 2020 22:17:24 +0100 Subject: [PATCH 355/563] add backtesting results abs profit min/abs profit max, to get a better view if a strategy has a enough money to succeed --- freqtrade/data/btanalysis.py | 18 ++++++++++++++++++ freqtrade/optimize/optimize_reports.py | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 828fb78f3..8e851a8e8 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -383,3 +383,21 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col] low_date = profit_results.loc[idxmin, date_col] return abs(min(max_drawdown_df['drawdown'])), high_date, low_date + + +def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: + """ + Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane + :param trades: DataFrame containing trades (requires columns close_date and profit_percent) + :return: Tuple (float, float) with cumsum of profit_abs + :raise: ValueError if trade-dataframe was found empty. + """ + if len(trades) == 0: + raise ValueError("Trade dataframe empty.") + + csum_df = pd.DataFrame() + csum_df['sum'] = trades['profit_abs'].cumsum() + csum_min = csum_df['sum'].min() + csum_max = csum_df['sum'].max() + + return csum_min, csum_max diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 118253e86..88b2028ba 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -9,7 +9,8 @@ from pandas import DataFrame from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN -from freqtrade.data.btanalysis import calculate_market_change, calculate_max_drawdown +from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change, + calculate_max_drawdown) from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value @@ -324,6 +325,13 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, }) + + csum_min, csum_max = calculate_csum(results) + strat_stats.update({ + 'csum_min': csum_min, + 'csum_max': csum_max + }) + except ValueError: strat_stats.update({ 'max_drawdown': 0.0, @@ -331,6 +339,8 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'drawdown_start_ts': 0, 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_end_ts': 0, + 'csum_min': 0, + 'csum_max': 0 }) strategy_results = generate_strategy_metrics(all_results=all_results) @@ -439,6 +449,12 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('', ''), # Empty line to improve readability + + ('Abs Profit Min', round_coin_value(strat_results['csum_min'], + strat_results['stake_currency'])), + ('Abs Profit Max', round_coin_value(strat_results['csum_max'], + strat_results['stake_currency'])), + ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), From 1a166f639d79d718984c9d6df19ce70d078cfeaa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 19:44:13 +0100 Subject: [PATCH 356/563] Add test for calcuate_csum --- tests/data/test_btanalysis.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 96ac6f63c..3c4687745 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -8,11 +8,12 @@ from pandas import DataFrame, DateOffset, Timestamp, to_datetime from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_DATA_COLUMNS_OLD, - analyze_trade_parallelism, calculate_market_change, - calculate_max_drawdown, combine_dataframes_with_mean, - create_cum_profit, extract_trades_of_period, - get_latest_backtest_filename, get_latest_hyperopt_file, - load_backtest_data, load_trades, load_trades_from_db) + analyze_trade_parallelism, calculate_csum, + calculate_market_change, calculate_max_drawdown, + combine_dataframes_with_mean, create_cum_profit, + extract_trades_of_period, get_latest_backtest_filename, + get_latest_hyperopt_file, load_backtest_data, load_trades, + load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history from tests.conftest import create_mock_trades from tests.conftest_trades import MOCK_TRADE_COUNT @@ -284,6 +285,20 @@ def test_calculate_max_drawdown(testdatadir): drawdown, h, low = calculate_max_drawdown(DataFrame()) +def test_calculate_csum(testdatadir): + filename = testdatadir / "backtest-result_test.json" + bt_data = load_backtest_data(filename) + csum_min, csum_max = calculate_csum(bt_data) + + assert isinstance(csum_min, float) + assert isinstance(csum_max, float) + assert csum_min < 0.01 + assert csum_max > 0.02 + + with pytest.raises(ValueError, match='Trade dataframe empty.'): + csum_min, csum_max = calculate_csum(DataFrame()) + + def test_calculate_max_drawdown2(): values = [0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024, -0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872, From 44cb2066889e5b62974c0b83f377d900e5f8daef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Feb 2021 05:27:01 +0000 Subject: [PATCH 357/563] Bump joblib from 1.0.0 to 1.0.1 Bumps [joblib](https://github.com/joblib/joblib) from 1.0.0 to 1.0.1. - [Release notes](https://github.com/joblib/joblib/releases) - [Changelog](https://github.com/joblib/joblib/blob/master/CHANGES.rst) - [Commits](https://github.com/joblib/joblib/compare/1.0.0...1.0.1) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 104fbf454..535283b55 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,5 +6,5 @@ scipy==1.6.0 scikit-learn==0.24.1 scikit-optimize==0.8.1 filelock==3.0.12 -joblib==1.0.0 +joblib==1.0.1 progressbar2==3.53.1 From d08572ea0d7613c15bd6fc8e54c457d6e9672771 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Feb 2021 05:27:05 +0000 Subject: [PATCH 358/563] Bump pandas from 1.2.1 to 1.2.2 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.2.1...v1.2.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5d03a9c2d..cce21ccc7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.20.1 -pandas==1.2.1 +pandas==1.2.2 ccxt==1.41.70 # Pin cryptography for now due to rust build errors with piwheels From dbef5425c591305f62b6fbfd319e4cae9e9322d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Feb 2021 05:27:12 +0000 Subject: [PATCH 359/563] Bump prompt-toolkit from 3.0.14 to 3.0.16 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.14 to 3.0.16. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.14...3.0.16) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5d03a9c2d..f9d224fcb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,4 +39,4 @@ aiofiles==0.6.0 colorama==0.4.4 # Building config files interactively questionary==1.9.0 -prompt-toolkit==3.0.14 +prompt-toolkit==3.0.16 From 5f2513934898c8b99bba7b010027e5f02e1a3bbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Feb 2021 08:18:04 +0000 Subject: [PATCH 360/563] Bump ccxt from 1.41.70 to 1.41.90 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.41.70 to 1.41.90. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.41.70...1.41.90) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d0586b820..b84df9b2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.2 -ccxt==1.41.70 +ccxt==1.41.90 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.3.2 aiohttp==3.7.3 From bc188907b8760ff80906857939be8071258d9192 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Feb 2021 08:46:46 +0000 Subject: [PATCH 361/563] Bump cryptography from 3.3.2 to 3.4.5 Bumps [cryptography](https://github.com/pyca/cryptography) from 3.3.2 to 3.4.5. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.3.2...3.4.5) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b84df9b2a..48c5fd475 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.2.2 ccxt==1.41.90 # Pin cryptography for now due to rust build errors with piwheels -cryptography==3.3.2 +cryptography==3.4.5 aiohttp==3.7.3 SQLAlchemy==1.3.23 python-telegram-bot==13.2 From 3e06cd8b3a38231dc967ec7b3a7f6405995ad536 Mon Sep 17 00:00:00 2001 From: Florian Merz Date: Tue, 16 Feb 2021 10:11:33 +0100 Subject: [PATCH 362/563] pass data and config to loss function --- freqtrade/optimize/hyperopt.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d0cdceaeb..83bdbad17 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -546,10 +546,11 @@ class Hyperopt: ) return self._get_results_dict(backtesting_results, min_date, max_date, - params_dict, params_details) + params_dict, params_details, + processed=processed) def _get_results_dict(self, backtesting_results, min_date, max_date, - params_dict, params_details): + params_dict, params_details, processed: Dict): results_metrics = self._calculate_results_metrics(backtesting_results) results_explanation = self._format_results_explanation_string(results_metrics) @@ -563,7 +564,8 @@ class Hyperopt: loss: float = MAX_LOSS if trade_count >= self.config['hyperopt_min_trades']: loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count, - min_date=min_date.datetime, max_date=max_date.datetime) + min_date=min_date.datetime, max_date=max_date.datetime, + config=self.config, processed=processed) return { 'loss': loss, 'params_dict': params_dict, From 009a447d8a59f791b230537fa6e0d1f8a923245c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Feb 2021 19:51:09 +0100 Subject: [PATCH 363/563] Adjust documentation for new parameter in loss functions --- docs/advanced-hyperopt.md | 7 +++++++ freqtrade/optimize/hyperopt.py | 2 +- freqtrade/templates/sample_hyperopt_loss.py | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index bead18038..50d1946aa 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -40,6 +40,11 @@ For the sample below, you then need to add the command line parameter `--hyperop A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found in [userdata/hyperopts](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_loss.py). ``` python +from datetime import datetime +from typing import Dict + +from pandas import DataFrame + from freqtrade.optimize.hyperopt import IHyperOptLoss TARGET_TRADES = 600 @@ -54,6 +59,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): @staticmethod def hyperopt_loss_function(results: DataFrame, trade_count: int, min_date: datetime, max_date: datetime, + processed: Dict[str, DataFrame], *args, **kwargs) -> float: """ Objective function, returns smaller number for better results @@ -81,6 +87,7 @@ Currently, the arguments are: * `trade_count`: Amount of trades (identical to `len(results)`) * `min_date`: Start date of the timerange used * `min_date`: End date of the timerange used +* `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting. This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you. diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 83bdbad17..eee0f13b3 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -550,7 +550,7 @@ class Hyperopt: processed=processed) def _get_results_dict(self, backtesting_results, min_date, max_date, - params_dict, params_details, processed: Dict): + params_dict, params_details, processed: Dict[str, DataFrame]): results_metrics = self._calculate_results_metrics(backtesting_results) results_explanation = self._format_results_explanation_string(results_metrics) diff --git a/freqtrade/templates/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py index a2b28f948..389c811f8 100644 --- a/freqtrade/templates/sample_hyperopt_loss.py +++ b/freqtrade/templates/sample_hyperopt_loss.py @@ -1,5 +1,6 @@ from datetime import datetime from math import exp +from typing import Dict from pandas import DataFrame @@ -35,6 +36,7 @@ class SampleHyperOptLoss(IHyperOptLoss): @staticmethod def hyperopt_loss_function(results: DataFrame, trade_count: int, min_date: datetime, max_date: datetime, + processed: Dict[str, DataFrame], *args, **kwargs) -> float: """ Objective function, returns smaller number for better results From 11b20d693256d3d6a75c983bf52060d06614b714 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 07:04:29 +0100 Subject: [PATCH 364/563] Add config to hyperopt_loss_function documentation --- docs/advanced-hyperopt.md | 3 ++- freqtrade/optimize/hyperopt_loss_interface.py | 5 ++++- freqtrade/templates/sample_hyperopt_loss.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 50d1946aa..d2237b3e8 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -59,7 +59,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): @staticmethod def hyperopt_loss_function(results: DataFrame, trade_count: int, min_date: datetime, max_date: datetime, - processed: Dict[str, DataFrame], + config: Dict, processed: Dict[str, DataFrame], *args, **kwargs) -> float: """ Objective function, returns smaller number for better results @@ -87,6 +87,7 @@ Currently, the arguments are: * `trade_count`: Amount of trades (identical to `len(results)`) * `min_date`: Start date of the timerange used * `min_date`: End date of the timerange used +* `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space). * `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting. This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you. diff --git a/freqtrade/optimize/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss_interface.py index 48407a8a8..b5aa588b2 100644 --- a/freqtrade/optimize/hyperopt_loss_interface.py +++ b/freqtrade/optimize/hyperopt_loss_interface.py @@ -5,6 +5,7 @@ This module defines the interface for the loss-function for hyperopt from abc import ABC, abstractmethod from datetime import datetime +from typing import Dict from pandas import DataFrame @@ -19,7 +20,9 @@ class IHyperOptLoss(ABC): @staticmethod @abstractmethod def hyperopt_loss_function(results: DataFrame, trade_count: int, - min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + min_date: datetime, max_date: datetime, + config: Dict, processed: Dict[str, DataFrame], + *args, **kwargs) -> float: """ Objective function, returns smaller number for better results """ diff --git a/freqtrade/templates/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py index 389c811f8..343349508 100644 --- a/freqtrade/templates/sample_hyperopt_loss.py +++ b/freqtrade/templates/sample_hyperopt_loss.py @@ -36,7 +36,7 @@ class SampleHyperOptLoss(IHyperOptLoss): @staticmethod def hyperopt_loss_function(results: DataFrame, trade_count: int, min_date: datetime, max_date: datetime, - processed: Dict[str, DataFrame], + config: Dict, processed: Dict[str, DataFrame], *args, **kwargs) -> float: """ Objective function, returns smaller number for better results From fedbb5c0c47d9a0b1098869f4d516e7a63378d10 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 20:46:58 +0100 Subject: [PATCH 365/563] Remove last flask occurance from setup.py fixes #4390 --- setup.py | 2 +- tests/rpc/test_rpc_apiserver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 030980c96..148803cd6 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ if readme_file.is_file(): readme_long = (Path(__file__).parent / "README.md").read_text() # Requirements used for submodules -api = ['flask', 'flask-jwt-extended', 'flask-cors'] +api = ['fastapi', 'uvicorn', 'pyjwt', 'aiofiles'] plot = ['plotly>=4.0'] hyperopt = [ 'scipy', diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 263950d83..3cc0d0137 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -295,7 +295,7 @@ def test_api_run(default_conf, mocker, caplog): "Please make sure that this is intentional!", caplog) assert log_has_re("SECURITY WARNING - `jwt_secret_key` seems to be default.*", caplog) - # Test crashing flask + # Test crashing API server caplog.clear() mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer', MagicMock(side_effect=Exception)) From 87dc1d39551c3b267db0cc524f8a00a0c58ab86f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 20:52:25 +0100 Subject: [PATCH 366/563] Explicitly push tag and tag_plot images --- build_helpers/publish_docker.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 9bc1aa0a6..d987bcc69 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -51,6 +51,8 @@ fi docker images docker push ${IMAGE_NAME} +docker push ${IMAGE_NAME}:$TAG_PLOT +docker push ${IMAGE_NAME}:$TAG if [ $? -ne 0 ]; then echo "failed pushing repo" return 1 From b5a9ce28944a3c01e2693ff03a27416fc22cd11c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Feb 2021 09:26:35 +0100 Subject: [PATCH 367/563] Download data in the right format as well ... closes #4393 --- freqtrade/edge/edge_positioning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 2bdef1c89..ff86e522e 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -104,6 +104,7 @@ class Edge: exchange=self.exchange, timeframe=self.strategy.timeframe, timerange=self._timerange, + data_format=self.config.get('dataformat_ohlcv', 'json'), ) data = load_data( From 327c23618f80c195048995b359d93965f8ccd208 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Feb 2021 09:30:35 +0100 Subject: [PATCH 368/563] Improve documentation for get_analyzed_dataframe --- docs/strategy-customization.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 7e998570f..4eb76a617 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -444,14 +444,19 @@ It can also be used in specific callbacks to get the signal that caused the acti ``` python # fetch current dataframe if self.dp: - dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'], - timeframe=self.timeframe) + if self.dp.runmode.value in ('live', 'dry_run'): + dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'], + timeframe=self.timeframe) ``` !!! Note "No data available" Returns an empty dataframe if the requested pair was not cached. This should not happen when using whitelisted pairs. + +!!! Warning "Warning about backtesting" + This method will return an empty dataframe during backtesting. + ### *orderbook(pair, maximum)* ``` python @@ -462,8 +467,8 @@ if self.dp: dataframe['best_ask'] = ob['asks'][0][0] ``` -!!! Warning - The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used. +!!! Warning "Warning about backtesting" + The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return uptodate values. ### *ticker(pair)* From 2b0d2070d0b1b7428ec12f433abe1d213e1a884d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Feb 2021 12:49:14 +0100 Subject: [PATCH 369/563] Avoid crash with /delete When a trade is deleted between querying the database and actually handling the trade. closes #4326 --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 73707e4ec..d546dd6d2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -179,6 +179,7 @@ class FreqtradeBot(LoggingMixin): # Without this, freqtrade my try to recreate stoploss_on_exchange orders # while selling is in process, since telegram messages arrive in an different thread. with self._sell_lock: + trades = Trade.get_open_trades() # First process current opened trades (positions) self.exit_positions(trades) From c9688f1c8912f16c1a737baad8479f7c3aa3743e Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 18 Feb 2021 17:30:29 +0100 Subject: [PATCH 370/563] fix(doc/plotting): misplaced comma in example code --- docs/plotting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plotting.md b/docs/plotting.md index 19ddb4f57..d7ed5ab1f 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -188,7 +188,7 @@ Sample configuration with inline comments explaining the process: 'senkou_a': { 'color': 'green', #optional 'fill_to': 'senkou_b', - 'fill_label': 'Ichimoku Cloud' #optional, + 'fill_label': 'Ichimoku Cloud', #optional 'fill_color': 'rgba(255,76,46,0.2)', #optional }, # plot senkou_b, too. Not only the area to it. From 245e39e523d8239ffbdf881e49290967592cf6e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 19:17:10 +0100 Subject: [PATCH 371/563] dry-run should be a bool, not a string --- freqtrade/rpc/api_server/api_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 4faefb5fc..050540cc6 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -113,7 +113,7 @@ class Daily(BaseModel): class ShowConfig(BaseModel): - dry_run: str + dry_run: bool stake_currency: str stake_amount: Union[float, str] max_open_trades: int From 3629892fc35fec3b6daaa2938371d4c1614cbe32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 19:37:38 +0100 Subject: [PATCH 372/563] Stoploss-guard should use the trade_limit or more fix #4404 --- docs/includes/protections.md | 4 +++- freqtrade/plugins/protections/stoploss_guard.py | 12 ++++++------ tests/plugins/test_protections.py | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index de34383ac..6bc57153e 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -40,7 +40,9 @@ All protection end times are rounded up to the next candle to avoid sudden, unex #### Stoploss Guard -`StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`). +`StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`). +If `trade_limit` or more trades resulted in stoploss, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`). + This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles. diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 92fae54cb..5a9b9ddd0 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -58,13 +58,13 @@ class StoplossGuard(IProtection): SellType.STOPLOSS_ON_EXCHANGE.value) and trade.close_profit < 0)] - if len(trades) > self._trade_limit: - self.log_once(f"Trading stopped due to {self._trade_limit} " - f"stoplosses within {self._lookback_period} minutes.", logger.info) - until = self.calculate_lock_end(trades, self._stop_duration) - return True, until, self._reason() + if len(trades) < self._trade_limit: + return False, None, None - return False, None, None + self.log_once(f"Trading stopped due to {self._trade_limit} " + f"stoplosses within {self._lookback_period} minutes.", logger.info) + until = self.calculate_lock_end(trades, self._stop_duration) + return True, until, self._reason() def global_stop(self, date_now: datetime) -> ProtectionReturn: """ diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index e36900a96..2e42c1be4 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -83,7 +83,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): "method": "StoplossGuard", "lookback_period": 60, "stop_duration": 40, - "trade_limit": 2 + "trade_limit": 3 }] freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to .*" @@ -136,7 +136,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair default_conf['protections'] = [{ "method": "StoplossGuard", "lookback_period": 60, - "trade_limit": 1, + "trade_limit": 2, "stop_duration": 60, "only_per_pair": only_per_pair }] From 188d7aaf8c8da10e67ecaf0b8a61d907339a6597 Mon Sep 17 00:00:00 2001 From: Alberto del Barrio Date: Sun, 21 Feb 2021 18:50:11 +0100 Subject: [PATCH 373/563] Fix example in storing-information docs --- docs/strategy-customization.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 4eb76a617..fdc95a3c1 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -315,11 +315,11 @@ class AwesomeStrategy(IStrategy): def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Check if the entry already exists - if not metadata["pair"] in self._cust_info: + if not metadata["pair"] in self.cust_info: # Create empty entry for this pair - self._cust_info[metadata["pair"]] = {} + self.cust_info[metadata["pair"]] = {} - if "crosstime" in self.cust_info[metadata["pair"]: + if "crosstime" in self.cust_info[metadata["pair"]]: self.cust_info[metadata["pair"]]["crosstime"] += 1 else: self.cust_info[metadata["pair"]]["crosstime"] = 1 From ab74c6e77154a588c76e6628a6f39318c19c0b15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 05:27:41 +0000 Subject: [PATCH 374/563] Bump tabulate from 0.8.7 to 0.8.8 Bumps [tabulate](https://github.com/astanin/python-tabulate) from 0.8.7 to 0.8.8. - [Release notes](https://github.com/astanin/python-tabulate/releases) - [Changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG) - [Commits](https://github.com/astanin/python-tabulate/compare/v0.8.7...v0.8.8) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 48c5fd475..1033c42d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ urllib3==1.26.3 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 -tabulate==0.8.7 +tabulate==0.8.8 pycoingecko==1.4.0 jinja2==2.11.3 tables==3.6.1 From 8c398acc098b8bd9f15666e18ad9b9cd6811071f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 05:27:42 +0000 Subject: [PATCH 375/563] Bump python-telegram-bot from 13.2 to 13.3 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.2 to 13.3. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.2...v13.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 48c5fd475..8387be19c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==1.41.90 cryptography==3.4.5 aiohttp==3.7.3 SQLAlchemy==1.3.23 -python-telegram-bot==13.2 +python-telegram-bot==13.3 arrow==0.17.0 cachetools==4.2.1 requests==2.25.1 From 932aabd0121bd95f6ac28e7a016faf0ca324f498 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 05:27:51 +0000 Subject: [PATCH 376/563] Bump uvicorn from 0.13.3 to 0.13.4 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.13.3 to 0.13.4. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.13.3...0.13.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 48c5fd475..de39a219e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ sdnotify==0.3.2 # API Server fastapi==0.63.0 -uvicorn==0.13.3 +uvicorn==0.13.4 pyjwt==2.0.1 aiofiles==0.6.0 From dea04c64521fd3f36f58ae6b603485714b1b62d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 05:27:54 +0000 Subject: [PATCH 377/563] Bump scipy from 1.6.0 to 1.6.1 Bumps [scipy](https://github.com/scipy/scipy) from 1.6.0 to 1.6.1. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.6.0...v1.6.1) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 535283b55..8e87a434c 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.6.0 +scipy==1.6.1 scikit-learn==0.24.1 scikit-optimize==0.8.1 filelock==3.0.12 From 85f12f8c284b2230606a11c7aa9f23e8eefc85bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 05:27:57 +0000 Subject: [PATCH 378/563] Bump cryptography from 3.4.5 to 3.4.6 Bumps [cryptography](https://github.com/pyca/cryptography) from 3.4.5 to 3.4.6. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.4.5...3.4.6) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 48c5fd475..a7b5eaf7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.2.2 ccxt==1.41.90 # Pin cryptography for now due to rust build errors with piwheels -cryptography==3.4.5 +cryptography==3.4.6 aiohttp==3.7.3 SQLAlchemy==1.3.23 python-telegram-bot==13.2 From d8c7e5ce8d8c3e777e0d816b8894d5cb1067ffef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 05:56:25 +0000 Subject: [PATCH 379/563] Bump python from 3.9.1-slim-buster to 3.9.2-slim-buster Bumps python from 3.9.1-slim-buster to 3.9.2-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8d4f0ebe6..4b399174b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.1-slim-buster as base +FROM python:3.9.2-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/Dockerfile.armhf b/Dockerfile.armhf index f938ec457..f46212332 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.9-slim-buster as base +FROM --platform=linux/arm/v7 python:3.9.2-slim-buster as base # Setup env ENV LANG C.UTF-8 From 8a62bfa0e5fc49cd3c9b1b49436c03d9ffbc0abe Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Feb 2021 08:20:45 +0100 Subject: [PATCH 380/563] armhf image should not be updated to python3.9 --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index f46212332..f938ec457 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.9.2-slim-buster as base +FROM --platform=linux/arm/v7 python:3.7.9-slim-buster as base # Setup env ENV LANG C.UTF-8 From 5e4730b73b2f0b7a2ba51a7ab79bea962e973ec4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Feb 2021 11:44:39 +0100 Subject: [PATCH 381/563] Add test confirming #4405 --- tests/rpc/test_rpc_apiserver.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3cc0d0137..c4fcf61ea 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -14,6 +14,7 @@ from fastapi.testclient import TestClient from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ +from freqtrade.exceptions import ExchangeError from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC @@ -789,6 +790,15 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'exchange': 'bittrex', }] + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) + + rc = client_get(client, f"{BASE_URI}/status") + assert_response(rc) + resp_values = rc.json() + assert len(resp_values) == 1 + assert resp_values[0]['profit_abs'] is None + def test_api_version(botclient): ftbot, client = botclient From 228e51b60b6a049e7ff2f447c5dd6630c3531fec Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Feb 2021 12:11:27 +0100 Subject: [PATCH 382/563] Fix #4405 --- freqtrade/rpc/api_server/webserver.py | 13 +++++++++++++ tests/rpc/test_rpc_apiserver.py | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index f3eaa1ebc..8a5c958e9 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -2,6 +2,7 @@ import logging from ipaddress import IPv4Address from typing import Any, Dict +import rapidjson import uvicorn from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -14,6 +15,17 @@ from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler logger = logging.getLogger(__name__) +class FTJSONResponse(JSONResponse): + media_type = "application/json" + + def render(self, content: Any) -> bytes: + """ + Use rapidjson for responses + Handles NaN and Inf / -Inf in a javascript way by default. + """ + return rapidjson.dumps(content).encode("utf-8") + + class ApiServer(RPCHandler): _rpc: RPC @@ -32,6 +44,7 @@ class ApiServer(RPCHandler): self.app = FastAPI(title="Freqtrade API", docs_url='/docs' if api_config.get('enable_openapi', False) else None, redoc_url=None, + default_response_class=FTJSONResponse, ) self.configure_app(self.app, self._config) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c4fcf61ea..d7d69d0ae 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -11,6 +11,7 @@ import uvicorn from fastapi import FastAPI from fastapi.exceptions import HTTPException from fastapi.testclient import TestClient +from numpy import isnan from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ @@ -797,7 +798,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert_response(rc) resp_values = rc.json() assert len(resp_values) == 1 - assert resp_values[0]['profit_abs'] is None + assert isnan(resp_values[0]['profit_abs']) def test_api_version(botclient): From c71ecd3680bdd35c5cb785f43e6f3f10ba823c66 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Feb 2021 20:04:36 +0100 Subject: [PATCH 383/563] Fix wrong pair-content in strategy-analysis notebook and documentation closes #4399 --- docs/strategy_analysis_example.md | 6 ++++-- freqtrade/templates/strategy_analysis_example.ipynb | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 90e39fd76..5c479aa0b 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -24,7 +24,7 @@ config["strategy"] = "SampleStrategy" # Location of the data data_location = Path(config['user_data_dir'], 'data', 'binance') # Pair to analyze - Only use one pair here -pair = "BTC_USDT" +pair = "BTC/USDT" ``` @@ -34,7 +34,9 @@ from freqtrade.data.history import load_pair_history candles = load_pair_history(datadir=data_location, timeframe=config["timeframe"], - pair=pair) + pair=pair, + data_format = "hdf5", + ) # Confirm success print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}") diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index c6e64c74e..491afbdd7 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -40,7 +40,7 @@ "# Location of the data\n", "data_location = Path(config['user_data_dir'], 'data', 'binance')\n", "# Pair to analyze - Only use one pair here\n", - "pair = \"BTC_USDT\"" + "pair = \"BTC/USDT\"" ] }, { @@ -54,7 +54,9 @@ "\n", "candles = load_pair_history(datadir=data_location,\n", " timeframe=config[\"timeframe\"],\n", - " pair=pair)\n", + " pair=pair,\n", + " data_format = \"hdf5\",\n", + " )\n", "\n", "# Confirm success\n", "print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n", From a0fa1e84fc95fca3c76576a1ae01a65d2f6da58e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 19:06:36 +0000 Subject: [PATCH 384/563] Bump ccxt from 1.41.90 to 1.42.19 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.41.90 to 1.42.19. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.41.90...1.42.19) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2144fb59c..8a28865c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.2 -ccxt==1.41.90 +ccxt==1.42.19 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.3 From 3612c786b5929388a8fac8b5da79b5476717be5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 19:06:39 +0000 Subject: [PATCH 385/563] Bump tabulate from 0.8.8 to 0.8.9 Bumps [tabulate](https://github.com/astanin/python-tabulate) from 0.8.8 to 0.8.9. - [Release notes](https://github.com/astanin/python-tabulate/releases) - [Changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG) - [Commits](https://github.com/astanin/python-tabulate/compare/v0.8.8...v0.8.9) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2144fb59c..39774e563 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ urllib3==1.26.3 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 -tabulate==0.8.8 +tabulate==0.8.9 pycoingecko==1.4.0 jinja2==2.11.3 tables==3.6.1 From d6d8678fd627c6a119c21d994c86ed7e17ff59b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Feb 2021 06:34:10 +0100 Subject: [PATCH 386/563] Fix missleading FAQ information --- docs/faq.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 8a0c61b29..87b0893bd 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -38,12 +38,11 @@ you can't say much from few trades. ### I’d like to make changes to the config. Can I do that without having to kill the bot? -Yes. You can edit your config, use the `/stop` command in Telegram, followed by `/reload_config` and the bot will run with the new config. +Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy. ### I want to improve the bot with a new strategy -That's great. We have a nice backtesting and hyperoptimization setup. See -the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). +That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). ### Is there a setting to only SELL the coins being held and not perform anymore BUYS? From 1f30c3d7f1314d387da8e546987158ba8ed7e132 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Feb 2021 06:46:07 +0100 Subject: [PATCH 387/563] Refresh slack link --- CONTRIBUTING.md | 2 +- README.md | 4 ++-- docs/developer.md | 2 +- docs/faq.md | 2 +- docs/index.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index afa41ed33..c29d6e632 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Few pointers for contributions: - New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. - PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). -If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. +If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index 7ef0d4ce7..c3a665c47 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ For any questions not covered by the documentation or for further information ab Please check out our [discord server](https://discord.gg/MA9v74M). -You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA). +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) @@ -178,7 +178,7 @@ to understand the requirements before sending your pull-requests. Coding is not a necessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. -**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Important:** Always create your PR against the `develop` branch, not `stable`. diff --git a/docs/developer.md b/docs/developer.md index c09e528bf..4b8c64530 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,7 +2,7 @@ This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running. -All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) where you can ask questions. +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) where you can ask questions. ## Documentation diff --git a/docs/faq.md b/docs/faq.md index 87b0893bd..93b806dca 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -142,7 +142,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: diff --git a/docs/index.md b/docs/index.md index db5088707..9d1a1532e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -81,7 +81,7 @@ For any questions not covered by the documentation or for further information ab Please check out our [discord server](https://discord.gg/MA9v74M). -You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA). +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). ## Ready to try? From 117f0064ed12945a415d53059d52ae9006ec29fc Mon Sep 17 00:00:00 2001 From: Th0masL Date: Thu, 25 Feb 2021 05:02:08 +0200 Subject: [PATCH 388/563] Allow changing the order_type for forcesell --- docs/configuration.md | 6 ++++-- docs/stoploss.md | 4 ++++ freqtrade/freqtradebot.py | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 00d2830e4..2b13bdd3d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -275,7 +275,7 @@ For example, if your strategy is using a 1h timeframe, and you only want to buy ### Understand order_types -The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. +The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using limit-orders, and create stoplosses using market orders. It also allows to set the @@ -287,7 +287,7 @@ the buy order is fulfilled. If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and `stoploss_on_exchange`) need to be present, otherwise the bot will fail to start. -For information on (`emergencysell`,`stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md) +For information on (`emergencysell`,`forcesell`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md) Syntax for Strategy: @@ -296,6 +296,7 @@ order_types = { "buy": "limit", "sell": "limit", "emergencysell": "market", + "forcesell": "market", "stoploss": "market", "stoploss_on_exchange": False, "stoploss_on_exchange_interval": 60, @@ -310,6 +311,7 @@ Configuration: "buy": "limit", "sell": "limit", "emergencysell": "market", + "forcesell": "market", "stoploss": "market", "stoploss_on_exchange": false, "stoploss_on_exchange_interval": 60 diff --git a/docs/stoploss.md b/docs/stoploss.md index 671e643b0..4a4391655 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -51,6 +51,10 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. +### forcesell + +`forcesell` is an optional value, which defaults to the same value as `sell` and is used when sending a `/forcesell` command from Telegram or from the Rest API. + ### emergencysell `emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d546dd6d2..be35380bb 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1156,6 +1156,9 @@ class FreqtradeBot(LoggingMixin): if sell_reason == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") + if sell_reason == SellType.FORCE_SELL: + # Force sells (default to the sell_type defined in the strategy, but we allow this value to be changed) + order_type = self.strategy.order_types.get("forcesell", order_type) amount = self._safe_sell_amount(trade.pair, trade.amount) time_in_force = self.strategy.order_time_in_force['sell'] From 006f31129e73fbe2ad994ef7be797395c640ae6a Mon Sep 17 00:00:00 2001 From: Th0masL Date: Thu, 25 Feb 2021 05:23:24 +0200 Subject: [PATCH 389/563] Reduced length of the line --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index be35380bb..2f64f3dac 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1157,7 +1157,8 @@ class FreqtradeBot(LoggingMixin): # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") if sell_reason == SellType.FORCE_SELL: - # Force sells (default to the sell_type defined in the strategy, but we allow this value to be changed) + # Force sells (default to the sell_type defined in the strategy, + # but we allow this value to be changed) order_type = self.strategy.order_types.get("forcesell", order_type) amount = self._safe_sell_amount(trade.pair, trade.amount) From 262394e112aa66f17797089a5a8bcb51e0eb4bdd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Feb 2021 19:24:30 +0100 Subject: [PATCH 390/563] Add psutils to support OOM Gracefull shutdown closes #4436, #4439 #3990 --- requirements-hyperopt.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 8e87a434c..8cdb6fd28 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -7,4 +7,5 @@ scikit-learn==0.24.1 scikit-optimize==0.8.1 filelock==3.0.12 joblib==1.0.1 +psutil==5.8.0 progressbar2==3.53.1 diff --git a/setup.py b/setup.py index 148803cd6..118bc8485 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ hyperopt = [ 'filelock', 'joblib', 'progressbar2', + 'psutil', ] develop = [ From 6d38a2e6598299949eeefc83919a0252abbae39f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Feb 2021 19:54:57 +0100 Subject: [PATCH 391/563] Small enhancements to docs --- docs/bot-basics.md | 16 ++++++++-------- docs/configuration.md | 4 ++-- mkdocs.yml | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 30a25d4fc..13694c316 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -4,14 +4,14 @@ This page provides you some basic concepts on how Freqtrade works and operates. ## Freqtrade terminology -* Strategy: Your trading strategy, telling the bot what to do. -* Trade: Open position. -* Open Order: Order which is currently placed on the exchange, and is not yet complete. -* Pair: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT). -* Timeframe: Candle length to use (e.g. `"5m"`, `"1h"`, ...). -* Indicators: Technical indicators (SMA, EMA, RSI, ...). -* Limit order: Limit orders which execute at the defined limit price or better. -* Market order: Guaranteed to fill, may move price depending on the order size. +* **Strategy**: Your trading strategy, telling the bot what to do. +* **Trade**: Open position. +* **Open Order**: Order which is currently placed on the exchange, and is not yet complete. +* **Pair**: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT). +* **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...). +* **Indicators**: Technical indicators (SMA, EMA, RSI, ...). +* **Limit order**: Limit orders which execute at the defined limit price or better. +* **Market order**: Guaranteed to fill, may move price depending on the order size. ## Fee handling diff --git a/docs/configuration.md b/docs/configuration.md index 00d2830e4..e84455c22 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -224,6 +224,8 @@ To allow the bot to trade all the available `stake_currency` in your account (mi !!! Note "When using Dry-Run Mode" When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. +--8<-- "includes/pricing.md" + ### Understand minimal_roi The `minimal_roi` configuration parameter is a JSON object where the key is a duration @@ -449,8 +451,6 @@ The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT" ``` ---8<-- "includes/pricing.md" - ## Using Dry-run mode We recommend starting the bot in the Dry-run mode to see how your bot will diff --git a/mkdocs.yml b/mkdocs.yml index 18fccc333..ca52627cb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,10 +19,10 @@ nav: - Backtesting: backtesting.md - Hyperopt: hyperopt.md - Utility Sub-commands: utils.md + - Plotting: plotting.md - Data Analysis: - Jupyter Notebooks: data-analysis.md - Strategy analysis: strategy_analysis_example.md - - Plotting: plotting.md - Exchange-specific Notes: exchanges.md - Advanced Topics: - Advanced Post-installation Tasks: advanced-setup.md From d877e3c1df72261f3b6c81ffca0fe9cab9148837 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Feb 2021 06:51:32 +0100 Subject: [PATCH 392/563] Fix failing CI due to unavailable pairs --- config_bittrex.json.example | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config_bittrex.json.example b/config_bittrex.json.example index 0f0bbec4b..172cfcfc3 100644 --- a/config_bittrex.json.example +++ b/config_bittrex.json.example @@ -41,13 +41,13 @@ "ETH/BTC", "LTC/BTC", "ETC/BTC", - "DASH/BTC", - "ZEC/BTC", + "RVN/BTC", + "CRO/BTC", "XLM/BTC", "XRP/BTC", "TRX/BTC", "ADA/BTC", - "XMR/BTC" + "DOT/BTC" ], "pair_blacklist": [ "DOGE/BTC" From c4979fd87fc121eabd897ec4e831fbc1489f2752 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Feb 2021 06:57:51 +0100 Subject: [PATCH 393/563] Add note to check configuration settings to docker quickstart part of #4441 --- docs/docker_quickstart.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 9cccfa93d..5c7be3dde 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -75,7 +75,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a 1. The configuration is now available as `user_data/config.json` 2. Copy a custom strategy to the directory `user_data/strategies/` -3. add the Strategy' class name to the `docker-compose.yml` file +3. Add the Strategy' class name to the `docker-compose.yml` file The `SampleStrategy` is run by default. @@ -90,6 +90,9 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li docker-compose up -d ``` +!!! Warning "Default configuration" + While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. + #### Monitoring the bot You can check for running instances with `docker-compose ps`. From 1b3b3891090483d40e78973a0147450388d0cbc6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Feb 2021 07:58:15 +0100 Subject: [PATCH 394/563] Remove binanceje, add ftx to config selector closes #4441 --- docs/exchanges.md | 3 --- freqtrade/commands/build_config_commands.py | 5 ++++- freqtrade/templates/base_config.json.j2 | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index d877e6da2..4b3833e72 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -92,9 +92,6 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll } ``` -!!! Note - Older versions of freqtrade may require this key to be added to `"ccxt_async_config"` as well. - ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 7bdbcc057..3c34ff162 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -93,10 +93,10 @@ def ask_user_config() -> Dict[str, Any]: "message": "Select exchange", "choices": [ "binance", - "binanceje", "binanceus", "bittrex", "kraken", + "ftx", Separator(), "other", ], @@ -173,6 +173,9 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None: arguments=selections) logger.info(f"Writing config to `{config_path}`.") + logger.info( + "Please make sure to check the configuration contents and adjust settings to your needs.") + config_path.write_text(config_text) diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index f920843b2..226bf1a81 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -57,7 +57,8 @@ "enabled": false, "listen_ip_address": "127.0.0.1", "listen_port": 8080, - "verbosity": "info", + "verbosity": "error", + "enable_openapi": false, "jwt_secret_key": "somethingrandom", "CORS_origins": [], "username": "", From 622ff771ec09ba7d8a7325c13a89922fc4cbd44d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Feb 2021 07:21:04 +0000 Subject: [PATCH 395/563] Bump aiohttp from 3.7.3 to 3.7.4 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.7.3 to 3.7.4. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.3...v3.7.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 51b1ed3d1..d17070e34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.2.2 ccxt==1.42.19 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 -aiohttp==3.7.3 +aiohttp==3.7.4 SQLAlchemy==1.3.23 python-telegram-bot==13.3 arrow==0.17.0 From 51d73a58892252c8860e1584eb982ba5b82e0efe Mon Sep 17 00:00:00 2001 From: Marco Seguri Date: Fri, 26 Feb 2021 11:11:27 +0100 Subject: [PATCH 396/563] Fix #4441 --- docs/docker_quickstart.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 5c7be3dde..017264569 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -1,5 +1,7 @@ # Using Freqtrade with Docker +This page explains how to run the bot with Docker. It is not meant to work out of the box. You'll still need to read through the documentation and understand how to properly configure it. + ## Install Docker Start by downloading and installing Docker CE for your platform: From fc69240e6dee03d482c85fe84b9d4cbe6b6b6fca Mon Sep 17 00:00:00 2001 From: Xanders Date: Fri, 26 Feb 2021 17:46:23 +0300 Subject: [PATCH 397/563] Add JSON-encoded webhooks --- freqtrade/rpc/webhook.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 5796201b5..6d9b718ff 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -28,6 +28,11 @@ class Webhook(RPCHandler): self._url = self._config['webhook']['url'] + self._format = self._config['webhook'].get('format', 'form') + + if self._format != 'form' and self._format != 'json': + raise NotImplementedError('Unknown webhook format `{}`, possible values are `form` (default) and `json`'.format(self._format)) + def cleanup(self) -> None: """ Cleanup pending module resources. @@ -66,7 +71,14 @@ class Webhook(RPCHandler): def _send_msg(self, payload: dict) -> None: """do the actual call to the webhook""" + if self._format == 'form': + kwargs = {'data': payload} + elif self._format == 'json': + kwargs = {'json': payload} + else: + raise NotImplementedError('Unknown format: {}'.format(self._format)) + try: - post(self._url, data=payload) + post(self._url, **kwargs) except RequestException as exc: logger.warning("Could not call webhook url. Exception: %s", exc) From a2cd3ed5ba0c17125787965cb5dd9e9528cce19f Mon Sep 17 00:00:00 2001 From: Xanders Date: Fri, 26 Feb 2021 17:59:38 +0300 Subject: [PATCH 398/563] Add documentation for JSON webhook format --- docs/webhook-config.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index db6d4d1ef..14ac7e7ee 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -40,6 +40,19 @@ Sample configuration (tested using IFTTT). The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert our event and key to the url. +You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use `"format": "form"` or `"format": "json"` respectively. Example configuration for Mattermost Cloud integration: + +```json + "webhook": { + "enabled": true, + "url": "https://.cloud.mattermost.com/hooks/", + "format": "json", + "webhookstatus": { + "text": "Status: {status}" + } + }, +``` + Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called. ### Webhookbuy From 52641aaa315ce6903cade30c9db904816f2cfabd Mon Sep 17 00:00:00 2001 From: Xanders Date: Fri, 26 Feb 2021 18:12:10 +0300 Subject: [PATCH 399/563] Add test for webhook JSON format --- tests/rpc/test_rpc_webhook.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 4ca547390..025b7326f 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -225,3 +225,14 @@ def test__send_msg(default_conf, mocker, caplog): mocker.patch("freqtrade.rpc.webhook.post", post) webhook._send_msg(msg) assert log_has('Could not call webhook url. Exception: ', caplog) + +def test__send_msg_with_json_format(default_conf, mocker, caplog): + default_conf["webhook"] = get_webhook_dict() + default_conf["webhook"]["format"] = "json" + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) + msg = {'text': 'Hello'} + post = MagicMock() + mocker.patch("freqtrade.rpc.webhook.post", post) + webhook._send_msg(msg) + + assert post.call_args[1] == {'json': msg} From 984e70d4e8dd8dc8e2303ac8997143b382c7f9ed Mon Sep 17 00:00:00 2001 From: Xanders Date: Fri, 26 Feb 2021 21:15:40 +0300 Subject: [PATCH 400/563] Add webhook result example to documentation --- docs/webhook-config.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 14ac7e7ee..4d2b31ec9 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -53,6 +53,8 @@ You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use }, ``` +The result would be e.g. `Status: running` message in the Mattermost channel. + Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called. ### Webhookbuy From 9a926c155df00784bb1345736b6519a2b16776bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Feb 2021 19:30:42 +0100 Subject: [PATCH 401/563] Add forcesell entry to full config --- config_full.json.example | 1 + 1 file changed, 1 insertion(+) diff --git a/config_full.json.example b/config_full.json.example index 6593750b4..9a613c0a1 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -49,6 +49,7 @@ "buy": "limit", "sell": "limit", "emergencysell": "market", + "forcesell": "market", "stoploss": "market", "stoploss_on_exchange": false, "stoploss_on_exchange_interval": 60 From 7281e794b4fe547b147774c72cf4ce2b82dee99c Mon Sep 17 00:00:00 2001 From: Xanders Date: Fri, 26 Feb 2021 21:31:33 +0300 Subject: [PATCH 402/563] Fix too long line at webhook.py --- freqtrade/rpc/webhook.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 6d9b718ff..5a30a9be8 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -31,7 +31,8 @@ class Webhook(RPCHandler): self._format = self._config['webhook'].get('format', 'form') if self._format != 'form' and self._format != 'json': - raise NotImplementedError('Unknown webhook format `{}`, possible values are `form` (default) and `json`'.format(self._format)) + raise NotImplementedError('Unknown webhook format `{}`, possible values are ' + '`form` (default) and `json`'.format(self._format)) def cleanup(self) -> None: """ From efa50be145eb08e4496d9c0114ffc14d5b2abcf2 Mon Sep 17 00:00:00 2001 From: Xanders Date: Fri, 26 Feb 2021 21:32:41 +0300 Subject: [PATCH 403/563] Fix blank lines rule at test_rpc_webhook.py --- tests/rpc/test_rpc_webhook.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 025b7326f..5361cd947 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -226,6 +226,7 @@ def test__send_msg(default_conf, mocker, caplog): webhook._send_msg(msg) assert log_has('Could not call webhook url. Exception: ', caplog) + def test__send_msg_with_json_format(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() default_conf["webhook"]["format"] = "json" From f0391d3761a1aec4f1ad2e03236d8552a4f135d7 Mon Sep 17 00:00:00 2001 From: Xanders Date: Fri, 26 Feb 2021 21:40:45 +0300 Subject: [PATCH 404/563] Better JSON webhook result description --- docs/webhook-config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 4d2b31ec9..2e41ad2cc 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -53,7 +53,7 @@ You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use }, ``` -The result would be e.g. `Status: running` message in the Mattermost channel. +The result would be POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel. Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called. From 642e3be7c549669e6068d694b875dfbe40661ee8 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Fri, 26 Feb 2021 23:17:59 +0100 Subject: [PATCH 405/563] Fix(strategy/interface.py): comment typo `advice_buy` -> `advise_buy` --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index da4ce6c50..8a0b27e96 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -659,7 +659,7 @@ class IStrategy(ABC): def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ Populates indicators for given candle (OHLCV) data (for multiple pairs) - Does not run advice_buy or advise_sell! + Does not run advise_buy or advise_sell! Used by optimize operations only, not during dry / live runs. Using .copy() to get a fresh copy of the dataframe for every strategy run. Has positive effects on memory usage for whatever reason - also when From 9968e4e49c58334c3541d2e5c153fc9eb266f4d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 08:26:05 +0100 Subject: [PATCH 406/563] Add warning about downloading data from kraken closes #4449 --- docs/exchanges.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 4b3833e72..2e5bdfadd 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -40,6 +40,10 @@ Due to the heavy rate-limiting applied by Kraken, the following configuration se }, ``` +!!! Warning "Downloading data from kraken" + Downloading kraken data will require significantly more memory (RAM) than any other exchange, as the trades-data needs to be converted into candles on your machine. + It will also take a long time, as freqtrade will need to download every single trade that happened on the exchange for the pair / timerange combination, therefore please be patient. + ## Bittrex ### Order types From f0a154692de57912c1d5d9596133c17808f152f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jan 2021 06:37:42 +0100 Subject: [PATCH 407/563] Wallets should use trade_proxy --- freqtrade/wallets.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index d7dcfd487..078bcd07e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -11,6 +11,7 @@ from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange from freqtrade.persistence import Trade +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -26,13 +27,14 @@ class Wallet(NamedTuple): class Wallets: - def __init__(self, config: dict, exchange: Exchange) -> None: + def __init__(self, config: dict, exchange: Exchange, skip_update: bool = False) -> None: self._config = config self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self.start_cap = config['dry_run_wallet'] self._last_wallet_refresh = 0 - self.update() + if not skip_update: + self.update() def get_free(self, currency: str) -> float: balance = self._wallets.get(currency) @@ -64,8 +66,8 @@ class Wallets: """ # Recreate _wallets to reset closed trade balances _wallets = {} - closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() - open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() + closed_trades = Trade.get_trades_proxy(is_open=False) + open_trades = Trade.get_trades_proxy(is_open=True) tot_profit = sum([trade.calc_profit() for trade in closed_trades]) tot_in_trades = sum([trade.stake_amount for trade in open_trades]) @@ -102,7 +104,7 @@ class Wallets: if currency not in balances: del self._wallets[currency] - def update(self, require_update: bool = True) -> None: + def update(self, require_update: bool = True, log: bool = True) -> None: """ Updates wallets from the configured version. By default, updates from the exchange. @@ -111,11 +113,12 @@ class Wallets: :param require_update: Allow skipping an update if balances were recently refreshed """ if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)): - if self._config['dry_run']: - self._update_dry() - else: + if (not self._config['dry_run'] or self._config.get('runmode') == RunMode.LIVE): self._update_live() - logger.info('Wallets synced.') + else: + self._update_dry() + if log: + logger.info('Wallets synced.') self._last_wallet_refresh = arrow.utcnow().int_timestamp def get_all_balances(self) -> Dict[str, Any]: From 9361aa1c95d5a408a4015e4ae5b04d29fd129b58 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jan 2021 07:06:58 +0100 Subject: [PATCH 408/563] Add wallets to backtesting --- freqtrade/optimize/backtesting.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3186313e1..b68732d5c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -28,6 +28,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper +from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) @@ -114,6 +115,8 @@ class Backtesting: if self.config.get('enable_protections', False): self.protections = ProtectionManager(self.config) + self.wallets = Wallets(self.config, self.exchange) + # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) # Load one (first) strategy @@ -176,6 +179,10 @@ class Backtesting: PairLocks.reset_locks() Trade.reset_trades() + def update_wallets(self): + if self.wallets: + self.wallets.update(log=False) + def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ Helper function to convert a processed dataframes into lists for performance reasons. @@ -276,8 +283,10 @@ class Backtesting: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = SellType.FORCE_SELL trade.close(sell_row[OPEN_IDX], show_msg=False) - trade.is_open = True - trades.append(trade) + # Deepcopy object to have wallets update correctly + trade1 = deepcopy(trade) + trade1.is_open = True + trades.append(trade1) return trades def backtest(self, processed: Dict, stake_amount: float, @@ -346,6 +355,7 @@ class Backtesting: and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): + self.update_wallets() # Enter trade trade = Trade( pair=pair, @@ -372,6 +382,7 @@ class Backtesting: trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured if trade_entry: + self.update_wallets() # logger.debug(f"{pair} - Backtesting sell {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) @@ -384,6 +395,7 @@ class Backtesting: tmp += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) + self.update_wallets() return trade_list_to_dataframe(trades) @@ -425,6 +437,7 @@ class Backtesting: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) + print(self.wallets.get_all_balances()) self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, From 4ce4eadc2366f6f58ab0811bc2fc01e2018627e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jan 2021 07:15:04 +0100 Subject: [PATCH 409/563] remove only ccxt objects when hyperopting --- freqtrade/optimize/hyperopt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index eee0f13b3..9cc5f2059 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -661,7 +661,9 @@ class Hyperopt: dump(preprocessed, self.data_pickle_file) # We don't need exchange instance anymore while running hyperopt - self.backtesting.exchange = None # type: ignore + self.backtesting.exchange._api = None # type: ignore + self.backtesting.exchange._api_async = None # type: ignore + # self.backtesting.exchange = None # type: ignore self.backtesting.pairlists = None # type: ignore self.backtesting.strategy.dp = None # type: ignore IStrategy.dp = None # type: ignore From b5177eadabe2a349382d7ee537aaae423942435f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Feb 2021 10:22:59 +0100 Subject: [PATCH 410/563] Extract close method for exchange --- freqtrade/exchange/exchange.py | 3 +++ freqtrade/optimize/hyperopt.py | 1 + 2 files changed, 4 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 617cd6c26..0e9a90548 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -147,6 +147,9 @@ class Exchange: """ Destructor - clean up async stuff """ + self.close() + + def close(self): logger.debug("Exchange object destroyed, closing async loop") if self._api_async and inspect.iscoroutinefunction(self._api_async.close): asyncio.get_event_loop().run_until_complete(self._api_async.close()) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9cc5f2059..155f1e69b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -661,6 +661,7 @@ class Hyperopt: dump(preprocessed, self.data_pickle_file) # We don't need exchange instance anymore while running hyperopt + self.backtesting.exchange.close() self.backtesting.exchange._api = None # type: ignore self.backtesting.exchange._api_async = None # type: ignore # self.backtesting.exchange = None # type: ignore From 712d503e6ca51acde5a676f833f073018d465cab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Feb 2021 10:30:50 +0100 Subject: [PATCH 411/563] Use sell-reason value in backtesting, not the enum object --- freqtrade/optimize/backtesting.py | 9 +++++---- freqtrade/optimize/optimize_reports.py | 2 +- freqtrade/persistence/models.py | 12 ++++++++++-- tests/optimize/test_backtest_detail.py | 2 +- tests/optimize/test_backtesting.py | 2 +- tests/optimize/test_optimize_reports.py | 2 +- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b68732d5c..718fd2c42 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -259,11 +259,11 @@ class Backtesting: sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: - trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60) - closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = sell.sell_type + trade.sell_reason = sell.sell_type.value + trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) + closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) trade.close(closerate, show_msg=False) return trade @@ -281,7 +281,7 @@ class Backtesting: sell_row = data[pair][-1] trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = SellType.FORCE_SELL + trade.sell_reason = SellType.FORCE_SELL.value trade.close(sell_row[OPEN_IDX], show_msg=False) # Deepcopy object to have wallets update correctly trade1 = deepcopy(trade) @@ -366,6 +366,7 @@ class Backtesting: fee_open=self.fee, fee_close=self.fee, is_open=True, + exchange='backtesting', ) # TODO: hacky workaround to avoid opening > max_open_trades # This emulates previous behaviour - not sure if this is correct diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 88b2028ba..6338b1d71 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -132,7 +132,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List tabular_data.append( { - 'sell_reason': reason.value, + 'sell_reason': reason, 'trades': count, 'wins': len(result[result['profit_abs'] > 0]), 'draws': len(result[result['profit_abs'] == 0]), diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index dff59819c..a05aa2c96 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -268,6 +268,14 @@ class Trade(_DECL_BASE): return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})') + @property + def open_date_utc(self): + return self.open_date.replace(tzinfo=timezone.utc) + + @property + def close_date_utc(self): + return self.close_date.replace(tzinfo=timezone.utc) + def to_json(self) -> Dict[str, Any]: return { 'trade_id': self.id, @@ -306,9 +314,9 @@ class Trade(_DECL_BASE): 'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'close_profit_abs': self.close_profit_abs, # Deprecated - 'trade_duration_s': (int((self.close_date - self.open_date).total_seconds()) + 'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds()) if self.close_date else None), - 'trade_duration': (int((self.close_date - self.open_date).total_seconds() // 60) + 'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60) if self.close_date else None), 'profit_ratio': self.close_profit, diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index daf7c2053..c9499cc42 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -514,6 +514,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] - assert res.sell_reason == trade.sell_reason + assert res.sell_reason == trade.sell_reason.value assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index c8d4338af..db14749c3 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -486,7 +486,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: 'trade_duration': [235, 40], 'profit_ratio': [0.0, 0.0], 'profit_abs': [0.0, 0.0], - 'sell_reason': [SellType.ROI, SellType.ROI], + 'sell_reason': [SellType.ROI.value, SellType.ROI.value], 'initial_stop_loss_abs': [0.0940005, 0.09272236], 'initial_stop_loss_ratio': [-0.1, -0.1], 'stop_loss_abs': [0.0940005, 0.09272236], diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 51a78c7cc..8b64c2764 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -265,7 +265,7 @@ def test_generate_sell_reason_stats(): 'wins': [2, 0, 0], 'draws': [0, 0, 0], 'losses': [0, 0, 1], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + 'sell_reason': [SellType.ROI.value, SellType.ROI.value, SellType.STOP_LOSS.value] } ) From e32b2097f0010127f0bbb095a3968ccc5c71f6c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Feb 2021 10:20:43 +0100 Subject: [PATCH 412/563] Use timestamp in UTC timezone for ROI comparisons --- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2f64f3dac..fd2f8bdd0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -932,7 +932,7 @@ class FreqtradeBot(LoggingMixin): Check and execute sell """ should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.utcnow(), buy, sell, + trade, sell_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 8a0b27e96..6d40e56cc 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -649,7 +649,7 @@ class IStrategy(ABC): :return: True if bot should sell at current rate """ # Check if time matches and current rate is above threshold - trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) + trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60) _, roi = self.min_roi_reached_entry(trade_dur) if roi is None: return False From 081b9be45c072d4d39f5672122d3c5dbbbf5aa07 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Feb 2021 10:49:47 +0100 Subject: [PATCH 413/563] use get_all_locks to get locks for backtest result --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/pairlock_middleware.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 718fd2c42..f37107767 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -442,7 +442,7 @@ class Backtesting: self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, - 'locks': PairLocks.locks, + 'locks': PairLocks.get_all_locks(), 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), } diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 8644146d8..f0048bb52 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -123,3 +123,11 @@ class PairLocks(): now = datetime.now(timezone.utc) return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now) + + @staticmethod + def get_all_locks() -> List[PairLock]: + + if PairLocks.use_db: + return PairLock.query.all() + else: + return PairLocks.locks From 20455de2a9533ebbf3990b9efe241a1bec1543e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Feb 2021 20:22:33 +0100 Subject: [PATCH 414/563] Small enhancements to docs --- docs/strategy-customization.md | 2 +- freqtrade/persistence/migrations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index fdc95a3c1..fd733c88e 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -709,7 +709,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished. !!! Warning - Locking pairs is not available during backtesting. + Manually locking pairs is not available during backtesting, only locks via Protections are allowed. #### Pair locking example diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index ed976c2a9..961363b0e 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -141,7 +141,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: inspector = inspect(engine) cols = inspector.get_columns('trades') - if 'orders' not in previous_tables: + if 'orders' not in previous_tables and 'trades' in previous_tables: logger.info('Moving open orders to Orders table.') migrate_open_orders_to_trades(engine) else: From 0faa6f84dcd6ee8d5990fd8f2bbe0c7fed80dac9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 19:23:11 +0100 Subject: [PATCH 415/563] Improve Wallet logging disabling for backtesting --- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/wallets.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f37107767..7f2ba60f2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -116,6 +116,7 @@ class Backtesting: self.protections = ProtectionManager(self.config) self.wallets = Wallets(self.config, self.exchange) + self.wallets._log = False # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) @@ -181,7 +182,7 @@ class Backtesting: def update_wallets(self): if self.wallets: - self.wallets.update(log=False) + self.wallets.update() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 078bcd07e..9562f34e6 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -29,6 +29,7 @@ class Wallets: def __init__(self, config: dict, exchange: Exchange, skip_update: bool = False) -> None: self._config = config + self._log = True self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self.start_cap = config['dry_run_wallet'] @@ -104,7 +105,7 @@ class Wallets: if currency not in balances: del self._wallets[currency] - def update(self, require_update: bool = True, log: bool = True) -> None: + def update(self, require_update: bool = True) -> None: """ Updates wallets from the configured version. By default, updates from the exchange. @@ -117,7 +118,7 @@ class Wallets: self._update_live() else: self._update_dry() - if log: + if self._log: logger.info('Wallets synced.') self._last_wallet_refresh = arrow.utcnow().int_timestamp From 0754a7a78f36d379d9332de0bfef3124780debe0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 19:33:39 +0100 Subject: [PATCH 416/563] total_open_trades_stake should support no-db mode --- freqtrade/persistence/models.py | 10 +++++++--- tests/conftest.py | 20 +++++++++++++------- tests/conftest_trades.py | 4 ++++ tests/test_persistence.py | 8 ++++++-- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a05aa2c96..f72705c34 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -671,9 +671,13 @@ class Trade(_DECL_BASE): Calculates total invested amount in open trades in stake currency """ - total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ - .filter(Trade.is_open.is_(True))\ - .scalar() + if Trade.use_db: + total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ + .filter(Trade.is_open.is_(True))\ + .scalar() + else: + total_open_stake_amount = sum( + t.stake_amount for t in Trade.get_trades_proxy(is_open=True)) return total_open_stake_amount or 0 @staticmethod diff --git a/tests/conftest.py b/tests/conftest.py index 61899dd53..946ae1fb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -183,28 +183,34 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: freqtrade.exchange.refresh_latest_ohlcv = lambda p: None -def create_mock_trades(fee): +def create_mock_trades(fee, use_db: bool = True): """ Create some fake trades ... """ + def add_trade(trade): + if use_db: + Trade.session.add(trade) + else: + Trade.trades.append(trade) + # Simulate dry_run entries trade = mock_trade_1(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_2(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_3(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_4(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_5(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_6(fee) - Trade.session.add(trade) + add_trade(trade) @pytest.fixture(autouse=True) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index fa9910b8d..6a42d04e3 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -28,6 +28,7 @@ def mock_trade_1(fee): amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.123, exchange='bittrex', open_order_id='dry_run_buy_12345', @@ -180,6 +181,7 @@ def mock_trade_4(fee): amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.123, exchange='bittrex', open_order_id='prod_buy_12345', @@ -230,6 +232,7 @@ def mock_trade_5(fee): amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.123, exchange='bittrex', strategy='SampleStrategy', @@ -281,6 +284,7 @@ def mock_trade_6(fee): amount_requested=2.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.15, exchange='bittrex', strategy='SampleStrategy', diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d0d29f142..1fced3e16 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1039,14 +1039,18 @@ def test_fee_updated(fee): @pytest.mark.usefixtures("init_persistence") -def test_total_open_trades_stakes(fee): +@pytest.mark.parametrize('use_db', [True, False]) +def test_total_open_trades_stakes(fee, use_db): + Trade.use_db = use_db res = Trade.total_open_trades_stakes() assert res == 0 - create_mock_trades(fee) + create_mock_trades(fee, use_db) res = Trade.total_open_trades_stakes() assert res == 0.004 + Trade.use_db = True + @pytest.mark.usefixtures("init_persistence") def test_get_overall_performance(fee): From 959ff990460acd0e137c0c2aaccbb6cfc1efd932 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 19:45:59 +0100 Subject: [PATCH 417/563] Add Dry-run wallet CLI option --- docs/backtesting.md | 4 ++++ docs/bot-usage.md | 4 ++++ docs/configuration.md | 2 +- docs/hyperopt.md | 6 +++++- freqtrade/commands/arguments.py | 6 +++--- freqtrade/commands/cli_options.py | 5 +++++ freqtrade/configuration/configuration.py | 4 +++- freqtrade/wallets.py | 1 + 8 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index a14c8f2e4..38d1af45a 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -16,6 +16,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--eps] [--dmmp] [--enable-protections] + [--dry-run-wallet DRY_RUN_WALLET] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -48,6 +49,9 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections + --dry-run-wallet DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] Provide a space-separated list of strategies to backtest. Please note that ticker-interval needs to be diff --git a/docs/bot-usage.md b/docs/bot-usage.md index c7fe8634d..4ff6168a0 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -56,6 +56,7 @@ optional arguments: usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [--db-url PATH] [--sd-notify] [--dry-run] + [--dry-run-wallet DRY_RUN_WALLET] optional arguments: -h, --help show this help message and exit @@ -66,6 +67,9 @@ optional arguments: --sd-notify Notify systemd service manager. --dry-run Enforce dry-run for trading (removes Exchange secrets and simulates trades). + --dry-run-wallet DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/configuration.md b/docs/configuration.md index 0163e1671..663d9c5b2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -49,7 +49,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
**Datatype:** String | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
**Datatype:** Boolean -| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.
*Defaults to `1000`.*
**Datatype:** Float +| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.
*Defaults to `1000`.*
**Datatype:** Float | `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions.
*Defaults to `false`.*
**Datatype:** Boolean | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ec155062f..ee3d75d0b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -43,7 +43,8 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [--dmmp] [--enable-protections] [-e INT] + [--dmmp] [--enable-protections] + [--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] [--random-state INT] [--min-trades INT] @@ -82,6 +83,9 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections + --dry-run-wallet DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. -e INT, --epochs INT Specify number of epochs (default: 100). --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] Specify which parameters to hyperopt. Space-separated diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index c64c11a18..88cec7b3e 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -14,18 +14,18 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_TRADE = ["db_url", "sd_notify", "dry_run"] +ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", ] ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", "max_open_trades", "stake_amount", "fee"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", - "enable_protections", + "enable_protections", "dry_run_wallet", "strategy_list", "export", "exportfilename"] ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", "position_stacking", "use_max_market_positions", - "enable_protections", + "enable_protections", "dry_run_wallet", "epochs", "spaces", "print_all", "print_colorized", "print_json", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 7dc85377d..90ebb5e6a 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -110,6 +110,11 @@ AVAILABLE_CLI_OPTIONS = { help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).', action='store_true', ), + "dry_run_wallet": Arg( + '--dry-run-wallet', + help='Starting balance, used for backtesting / hyperopt and dry-runs.', + type=float, + ), # Optimize common "timeframe": Arg( '-i', '--timeframe', '--ticker-interval', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 7bf3e6bf2..6295d01d4 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -232,7 +232,9 @@ class Configuration: self._args_to_config(config, argname='stake_amount', logstring='Parameter --stake-amount detected, ' 'overriding stake_amount to: {} ...') - + self._args_to_config(config, argname='dry_run_wallet', + logstring='Parameter --dry-run-wallet detected, ' + 'overriding dry_run_wallet to: {} ...') self._args_to_config(config, argname='fee', logstring='Parameter --fee detected, ' 'setting fee to: {} ...') diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 9562f34e6..f5ce4c102 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -158,6 +158,7 @@ class Wallets: Check if stake amount can be fulfilled with the available balance for the stake currency :return: float: Stake amount + :raise: DependencyException if balance is lower than stake-amount """ available_amount = self._get_available_stake_amount() From e4abe902fc924b30c41ddc67edb744eb2095951e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 20:37:55 +0100 Subject: [PATCH 418/563] Enable compounding for backtesting --- freqtrade/optimize/backtesting.py | 66 ++++++++++++++------------ freqtrade/optimize/hyperopt.py | 1 - tests/optimize/test_backtest_detail.py | 1 - tests/optimize/test_backtesting.py | 6 --- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7f2ba60f2..7ed5064e7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -17,7 +17,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, @@ -270,6 +270,30 @@ class Backtesting: return None + def _enter_trade(self, pair: str, row, max_open_trades: int, + open_trade_count: int) -> Optional[Trade]: + self.update_wallets() + try: + stake_amount = self.wallets.get_trade_stake_amount( + pair, max_open_trades - open_trade_count, None) + except DependencyException: + stake_amount = 0 + if stake_amount: + # Enter trade + trade = Trade( + pair=pair, + open_rate=row[OPEN_IDX], + open_date=row[DATE_IDX], + stake_amount=stake_amount, + amount=round(stake_amount / row[OPEN_IDX], 8), + fee_open=self.fee, + fee_close=self.fee, + is_open=True, + exchange='backtesting', + ) + return trade + return None + def handle_left_open(self, open_trades: Dict[str, List[Trade]], data: Dict[str, List[Tuple]]) -> List[Trade]: """ @@ -290,7 +314,7 @@ class Backtesting: trades.append(trade1) return trades - def backtest(self, processed: Dict, stake_amount: float, + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, enable_protections: bool = False) -> DataFrame: @@ -302,7 +326,6 @@ class Backtesting: Avoid extensive logging in this method and functions it calls. :param processed: a processed dictionary with format {pair, data} - :param stake_amount: amount to use for each trade :param start_date: backtesting timerange start datetime :param end_date: backtesting timerange end datetime :param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited @@ -310,10 +333,6 @@ class Backtesting: :param enable_protections: Should protections be enabled? :return: DataFrame with trades (results of backtesting) """ - logger.debug(f"Run backtest, stake_amount: {stake_amount}, " - f"start_date: {start_date}, end_date: {end_date}, " - f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" - ) trades: List[Trade] = [] self.prepare_backtest(enable_protections) @@ -356,30 +375,18 @@ class Backtesting: and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): - self.update_wallets() - # Enter trade - trade = Trade( - pair=pair, - open_rate=row[OPEN_IDX], - open_date=row[DATE_IDX], - stake_amount=stake_amount, - amount=round(stake_amount / row[OPEN_IDX], 8), - fee_open=self.fee, - fee_close=self.fee, - is_open=True, - exchange='backtesting', - ) - # TODO: hacky workaround to avoid opening > max_open_trades - # This emulates previous behaviour - not sure if this is correct - # Prevents buying if the trade-slot was freed in this candle - open_trade_count_start += 1 - open_trade_count += 1 - # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") - open_trades[pair].append(trade) - Trade.trades.append(trade) + trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start) + if trade: + # TODO: hacky workaround to avoid opening > max_open_trades + # This emulates previous behaviour - not sure if this is correct + # Prevents buying if the trade-slot was freed in this candle + open_trade_count_start += 1 + open_trade_count += 1 + # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") + open_trades[pair].append(trade) + Trade.trades.append(trade) for trade in open_trades[pair]: - # since indexes has been incremented before, we need to go one step back to # also check the buying candle for sell conditions. trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured @@ -431,7 +438,6 @@ class Backtesting: # Execute backtest and store results results = self.backtest( processed=preprocessed, - stake_amount=self.config['stake_amount'], start_date=min_date.datetime, end_date=max_date.datetime, max_open_trades=max_open_trades, diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 155f1e69b..79ecb6052 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -537,7 +537,6 @@ class Hyperopt: backtesting_results = self.backtesting.backtest( processed=processed, - stake_amount=self.config['stake_amount'], start_date=min_date.datetime, end_date=max_date.datetime, max_open_trades=self.max_open_trades, diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index c9499cc42..4d6605b9f 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -503,7 +503,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: min_date, max_date = get_timerange({pair: frame}) results = backtesting.backtest( processed=data_processed, - stake_amount=default_conf['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=10, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index db14749c3..620bd1df5 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -90,7 +90,6 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None: assert isinstance(processed, dict) results = backtesting.backtest( processed=processed, - stake_amount=config['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=1, @@ -111,7 +110,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'): min_date, max_date = get_timerange(processed) return { 'processed': processed, - 'stake_amount': conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, 'max_open_trades': 10, @@ -461,7 +459,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: min_date, max_date = get_timerange(processed) results = backtesting.backtest( processed=processed, - stake_amount=default_conf['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=10, @@ -523,7 +520,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None min_date, max_date = get_timerange(processed) results = backtesting.backtest( processed=processed, - stake_amount=default_conf['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=1, @@ -678,7 +674,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) min_date, max_date = get_timerange(processed) backtest_conf = { 'processed': processed, - 'stake_amount': default_conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, 'max_open_trades': 3, @@ -694,7 +689,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtest_conf = { 'processed': processed, - 'stake_amount': default_conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, 'max_open_trades': 1, From 8d61a263823943cdbdb911d2c40bc283ba415903 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Feb 2021 20:20:32 +0100 Subject: [PATCH 419/563] Allow dynamic stake for backtesting and hyperopt --- freqtrade/commands/optimize_commands.py | 14 +++++++++----- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtesting.py | 7 ++++--- tests/optimize/test_hyperopt.py | 8 ++++---- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 7411ca9c6..bf36972c4 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -3,7 +3,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -23,10 +23,14 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ RunMode.HYPEROPT: 'hyperoptimization', } if (method in no_unlimited_runmodes.keys() and - config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT): - raise DependencyException( - f'The value of `stake_amount` cannot be set as "{constants.UNLIMITED_STAKE_AMOUNT}" ' - f'for {no_unlimited_runmodes[method]}') + config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT and + config['max_open_trades'] != float('inf')): + pass + # config['dry_run_wallet'] = config['stake_amount'] * \ + # config['max_open_trades'] * (2 - config['tradable_balance_ratio']) + + # logger.warning(f"Changing dry-run-wallet to {config['dry_run_wallet']} " + # "(max_open_trades * stake_amount).") return config diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7ed5064e7..29559126b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -445,11 +445,11 @@ class Backtesting: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) - print(self.wallets.get_all_balances()) self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), + 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), } diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 620bd1df5..061bcbaa0 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -17,7 +17,7 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode @@ -242,8 +242,9 @@ def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_con '--strategy', 'DefaultStrategy', ] - with pytest.raises(DependencyException, match=r'.`stake_amount`.*'): - setup_optimize_configuration(get_args(args), RunMode.BACKTEST) + # TODO: does this test still make sense? + conf = setup_optimize_configuration(get_args(args), RunMode.BACKTEST) + assert isinstance(conf, dict) def test_start(mocker, fee, default_conf, caplog) -> None: diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 68eb3d6f7..88a4cea2d 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,7 +15,7 @@ from filelock import Timeout from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode @@ -140,9 +140,9 @@ def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_con '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', ] - - with pytest.raises(DependencyException, match=r'.`stake_amount`.*'): - setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + # TODO: does this test still make sense? + conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + assert isinstance(conf, dict) def test_hyperoptresolver(mocker, default_conf, caplog) -> None: From 35e6a9ab3aa70cad59d8cc75b50bedecdce56e0a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Feb 2021 09:01:05 +0100 Subject: [PATCH 420/563] Backtest-reports should calculate total gains based on starting capital --- docs/backtesting.md | 18 +++++++++++--- freqtrade/optimize/optimize_reports.py | 32 +++++++++++++++++-------- tests/conftest.py | 1 + tests/optimize/test_optimize_reports.py | 10 ++++---- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 38d1af45a..eab64a7a9 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -252,7 +252,10 @@ A backtesting result will look like that: | Max open trades | 3 | | | | | Total trades | 429 | -| Total Profit % | 152.41% | +| Starting capital | 0.01000000 BTC | +| End capital | 0.01762792 BTC | +| Absolute profit | 0.00762792 BTC | +| Total Profit % | 76.2% | | Trades per day | 3.575 | | | | | Best Pair | LSK/BTC 26.26% | @@ -261,6 +264,7 @@ A backtesting result will look like that: | Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | +| Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | @@ -328,7 +332,10 @@ It contains some useful key metrics about performance of your strategy on backte | Max open trades | 3 | | | | | Total trades | 429 | -| Total Profit % | 152.41% | +| Starting capital | 0.01000000 BTC | +| End capital | 0.01762792 BTC | +| Absolute profit | 0.00762792 BTC | +| Total Profit % | 76.2% | | Trades per day | 3.575 | | | | | Best Pair | LSK/BTC 26.26% | @@ -337,6 +344,7 @@ It contains some useful key metrics about performance of your strategy on backte | Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | +| Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | @@ -351,11 +359,15 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). - `Total trades`: Identical to the total trades of the backtest output table. -- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. +- `Starting capital`: Start capital - as given by dry-run-wallet (config or command line). +- `End capital`: Final capital - starting capital + absolute profit. +- `Absolute profit`: Profit made in stake currency. +- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. +- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 6338b1d71..d6adfdf50 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -56,12 +56,13 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: 'Wins', 'Draws', 'Losses'] -def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> Dict: +def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: """ Generate one result dict, with "first_column" as key. """ profit_sum = result['profit_ratio'].sum() - profit_total = profit_sum / max_open_trades + # (end-capital - starting capital) / starting capital + profit_total = result['profit_abs'].sum() / starting_balance return { 'key': first_column, @@ -88,13 +89,13 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: } -def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, +def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_balance: 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 starting_balance: Starting balance :param results: Dataframe containing the backtest results :param skip_nan: Print "left open" open trades :return: List of Dicts containing the metrics per pair @@ -107,10 +108,10 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t if skip_nan and result['profit_abs'].isnull().all(): continue - tabular_data.append(_generate_result_line(result, max_open_trades, pair)) + tabular_data.append(_generate_result_line(result, starting_balance, pair)) # Append Total - tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL')) + tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data @@ -159,7 +160,7 @@ def generate_strategy_metrics(all_results: Dict) -> List[Dict]: tabular_data = [] for strategy, results in all_results.items(): tabular_data.append(_generate_result_line( - results['results'], results['config']['max_open_trades'], strategy) + results['results'], results['config']['dry_run_wallet'], strategy) ) return tabular_data @@ -246,15 +247,16 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], continue config = content['config'] max_open_trades = min(config['max_open_trades'], len(btdata.keys())) + starting_balance = config['dry_run_wallet'] stake_currency = config['stake_currency'] pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - max_open_trades=max_open_trades, + starting_balance=starting_balance, 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, + starting_balance=starting_balance, results=results.loc[results['is_open']], skip_nan=True) daily_stats = generate_daily_stats(results) @@ -276,7 +278,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, - 'profit_total': results['profit_ratio'].sum() / max_open_trades, + 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, 'backtest_start_ts': min_date.int_timestamp * 1000, @@ -292,6 +294,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], + 'starting_balance': starting_balance, + 'dry_run_wallet': starting_balance, + 'final_balance': content['final_balance'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), @@ -431,6 +436,13 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), + ('Starting capital', round_coin_value(strat_results['starting_balance'], + strat_results['stake_currency'])), + ('End capital', round_coin_value(strat_results['final_balance'], + strat_results['stake_currency'])), + ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], + strat_results['stake_currency'])), + ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), ('', ''), # Empty line to improve readability diff --git a/tests/conftest.py b/tests/conftest.py index 946ae1fb5..6e70603b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -261,6 +261,7 @@ def get_default_conf(testdatadir): "20": 0.02, "0": 0.04 }, + "dry_run_wallet": 1000, "stoploss": -0.10, "unfilledtimeout": { "buy": 10, diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 8b64c2764..405cc599b 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -48,7 +48,7 @@ def test_text_table_bt_results(): ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', - max_open_trades=2, results=results) + starting_balance=4, results=results) assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str @@ -78,6 +78,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): }), 'config': default_conf, 'locks': [], + 'final_balance': 1000.02, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, } @@ -189,7 +190,7 @@ def test_generate_pair_metrics(): ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', - max_open_trades=2, results=results) + starting_balance=2, results=results) assert isinstance(pair_results, list) assert len(pair_results) == 2 assert pair_results[-1]['key'] == 'TOTAL' @@ -291,6 +292,7 @@ def test_generate_sell_reason_stats(): def test_text_table_strategy(default_conf): default_conf['max_open_trades'] = 2 + default_conf['dry_run_wallet'] = 3 results = {} results['TestStrategy1'] = {'results': pd.DataFrame( { @@ -323,9 +325,9 @@ def test_text_table_strategy(default_conf): '|---------------+--------+----------------+----------------+------------------+' '----------------+----------------+--------+---------+----------|\n' '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |' - ' 30.00 | 0:17:00 | 3 | 0 | 0 |\n' + ' 36.67 | 0:17:00 | 3 | 0 | 0 |\n' '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' - ' 45.00 | 0:20:00 | 3 | 0 | 0 |' + ' 43.33 | 0:20:00 | 3 | 0 | 0 |' ) strategy_results = generate_strategy_metrics(all_results=results) From 72f21fc5ec90ae15f622c362d2a24bd31f068613 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 13:08:49 +0100 Subject: [PATCH 421/563] Add trade-volume metric --- docs/backtesting.md | 3 +++ freqtrade/optimize/optimize_reports.py | 5 ++++- tests/optimize/test_backtesting.py | 2 ++ tests/optimize/test_optimize_reports.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index eab64a7a9..ada788da9 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -257,6 +257,7 @@ A backtesting result will look like that: | Absolute profit | 0.00762792 BTC | | Total Profit % | 76.2% | | Trades per day | 3.575 | +| Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | | Worst Pair | ZEC/BTC -10.18% | @@ -337,6 +338,7 @@ It contains some useful key metrics about performance of your strategy on backte | Absolute profit | 0.00762792 BTC | | Total Profit % | 76.2% | | Trades per day | 3.575 | +| Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | | Worst Pair | ZEC/BTC -10.18% | @@ -364,6 +366,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Absolute profit`: Profit made in stake currency. - `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). +- `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d6adfdf50..dde0f8dd2 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -277,6 +277,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), + 'total_volume': results['stake_amount'].sum(), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), @@ -442,9 +443,11 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), - ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), + ('Total trade volume', round_coin_value(strat_results['total_volume'], + strat_results['stake_currency'])), + ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 061bcbaa0..8fba8724b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -817,6 +817,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '2018-01-30 05:35:00', ], utc=True), 'trade_duration': [235, 40], 'is_open': [False, False], + 'stake_amount': [0.01, 0.01], 'open_rate': [0.104445, 0.10302485], 'close_rate': [0.104969, 0.103541], 'sell_reason': [SellType.ROI, SellType.ROI] @@ -833,6 +834,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '2018-01-30 08:30:00'], utc=True), 'trade_duration': [47, 40, 20], 'is_open': [False, False, False], + 'stake_amount': [0.01, 0.01, 0.01], '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] diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 405cc599b..ca6a4ab01 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -73,6 +73,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], "trade_duration": [123, 34, 31, 14], "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], "sell_reason": [SellType.ROI, SellType.STOP_LOSS, SellType.ROI, SellType.FORCE_SELL] }), From 74fc4bdab5e108c72015f80112df8ff7aa12bdcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Feb 2021 07:56:35 +0100 Subject: [PATCH 422/563] Shorten debug log --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 29559126b..c60cfa9b7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -382,7 +382,7 @@ class Backtesting: # Prevents buying if the trade-slot was freed in this candle open_trade_count_start += 1 open_trade_count += 1 - # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") + # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") open_trades[pair].append(trade) Trade.trades.append(trade) From 0d2f877e77b17dc87c4efede8097dc90fd6202a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 19:30:17 +0100 Subject: [PATCH 423/563] Use absolute drawdown calc --- freqtrade/data/btanalysis.py | 10 ++++++--- freqtrade/optimize/optimize_reports.py | 17 ++++++++++++++- freqtrade/plot/plotting.py | 2 +- .../protections/max_drawdown_protection.py | 2 +- tests/data/test_btanalysis.py | 21 ++++++++++++------- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 8e851a8e8..117278585 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -360,13 +360,14 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date', value_col: str = 'profit_ratio' - ) -> Tuple[float, pd.Timestamp, pd.Timestamp]: + ) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float]: """ Calculate max drawdown and the corresponding close dates :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) :param date_col: Column in DataFrame to use for dates (defaults to 'close_date') :param value_col: Column in DataFrame to use for values (defaults to 'profit_ratio') - :return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time + :return: Tuple (float, highdate, lowdate, highvalue, lowvalue) with absolute max drawdown, + high and low time and high and low value. :raise: ValueError if trade-dataframe was found empty. """ if len(trades) == 0: @@ -382,7 +383,10 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' raise ValueError("No losing trade, therefore no drawdown.") high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col] low_date = profit_results.loc[idxmin, date_col] - return abs(min(max_drawdown_df['drawdown'])), high_date, low_date + high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin] + ['high_value'].idxmax(), 'cumulative'] + low_val = max_drawdown_df.loc[idxmin, 'cumulative'] + return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index dde0f8dd2..5b3f813f2 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -322,14 +322,20 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], result['strategy'][strategy] = strat_stats try: - max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( + max_drawdown, _, _, _, _ = calculate_max_drawdown( results, value_col='profit_ratio') + drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( + results, value_col='profit_abs') strat_stats.update({ 'max_drawdown': max_drawdown, + 'max_drawdown_abs': drawdown_abs, 'drawdown_start': drawdown_start, 'drawdown_start_ts': drawdown_start.timestamp() * 1000, 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, + + 'max_drawdown_low': low_val, + 'max_drawdown_high': high_val, }) csum_min, csum_max = calculate_csum(results) @@ -341,6 +347,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], except ValueError: strat_stats.update({ 'max_drawdown': 0.0, + 'max_drawdown_abs': 0.0, + 'max_drawdown_low': 0.0, + 'max_drawdown_high': 0.0, 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_start_ts': 0, 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), @@ -471,6 +480,12 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), + ('Max Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + strat_results['stake_currency'])), + ('Max Drawdown high', round_coin_value(strat_results['max_drawdown_high'], + strat_results['stake_currency'])), + ('Max Drawdown low', round_coin_value(strat_results['max_drawdown_low'], + strat_results['stake_currency'])), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 4325e537e..682c2b018 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -145,7 +145,7 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, Add scatter points indicating max drawdown """ try: - max_drawdown, highdate, lowdate = calculate_max_drawdown(trades) + max_drawdown, highdate, lowdate, _, _ = calculate_max_drawdown(trades) drawdown = go.Scatter( x=[highdate, lowdate], diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index d54e6699b..d1c6b192d 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -55,7 +55,7 @@ class MaxDrawdown(IProtection): # Drawdown is always positive try: - drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') + drawdown, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') except ValueError: return False, None, None diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 3c4687745..555808679 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -274,15 +274,17 @@ def test_create_cum_profit1(testdatadir): def test_calculate_max_drawdown(testdatadir): filename = testdatadir / "backtest-result_test.json" bt_data = load_backtest_data(filename) - drawdown, h, low = calculate_max_drawdown(bt_data) + drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(bt_data) assert isinstance(drawdown, float) assert pytest.approx(drawdown) == 0.21142322 - assert isinstance(h, Timestamp) - assert isinstance(low, Timestamp) - assert h == Timestamp('2018-01-24 14:25:00', tz='UTC') - assert low == Timestamp('2018-01-30 04:45:00', tz='UTC') + assert isinstance(hdate, Timestamp) + assert isinstance(lowdate, Timestamp) + assert isinstance(hval, float) + assert isinstance(lval, float) + assert hdate == Timestamp('2018-01-24 14:25:00', tz='UTC') + assert lowdate == Timestamp('2018-01-30 04:45:00', tz='UTC') with pytest.raises(ValueError, match='Trade dataframe empty.'): - drawdown, h, low = calculate_max_drawdown(DataFrame()) + drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(DataFrame()) def test_calculate_csum(testdatadir): @@ -310,13 +312,16 @@ def test_calculate_max_drawdown2(): # sort by profit and reset index df = df.sort_values('profit').reset_index(drop=True) df1 = df.copy() - drawdown, h, low = calculate_max_drawdown(df, date_col='open_date', value_col='profit') + drawdown, hdate, ldate, hval, lval = calculate_max_drawdown( + df, date_col='open_date', value_col='profit') # Ensure df has not been altered. assert df.equals(df1) assert isinstance(drawdown, float) # High must be before low - assert h < low + assert hdate < ldate + # High value must be higher than low value + assert hval > lval assert drawdown == 0.091755 df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date']) From aed23d55c280806af19fe9c7927fe1088ff1e0b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Feb 2021 20:12:59 +0100 Subject: [PATCH 424/563] Add starting balance to profit cumsum calculation --- freqtrade/data/btanalysis.py | 7 ++++--- tests/data/test_btanalysis.py | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 117278585..3adee8775 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -389,10 +389,11 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val -def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: +def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]: """ Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane :param trades: DataFrame containing trades (requires columns close_date and profit_percent) + :param starting_balance: Add starting balance to results, to show the wallets high / low points :return: Tuple (float, float) with cumsum of profit_abs :raise: ValueError if trade-dataframe was found empty. """ @@ -401,7 +402,7 @@ def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: csum_df = pd.DataFrame() csum_df['sum'] = trades['profit_abs'].cumsum() - csum_min = csum_df['sum'].min() - csum_max = csum_df['sum'].max() + csum_min = csum_df['sum'].min() + starting_balance + csum_max = csum_df['sum'].max() + starting_balance return csum_min, csum_max diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 555808679..538c89a90 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -296,6 +296,11 @@ def test_calculate_csum(testdatadir): assert isinstance(csum_max, float) assert csum_min < 0.01 assert csum_max > 0.02 + csum_min1, csum_max1 = calculate_csum(bt_data, 5) + + assert csum_min1 == csum_min + 5 + assert csum_max1 == csum_max + 5 + with pytest.raises(ValueError, match='Trade dataframe empty.'): csum_min, csum_max = calculate_csum(DataFrame()) From f367375e5b6240625ddd0ba51120ad48faa41409 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Feb 2021 20:39:50 +0100 Subject: [PATCH 425/563] ABS drawdown should show wallet high and low values --- freqtrade/optimize/optimize_reports.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 5b3f813f2..1ac0ae1d6 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -334,11 +334,11 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, - 'max_drawdown_low': low_val, - 'max_drawdown_high': high_val, + 'max_drawdown_low': low_val + starting_balance, + 'max_drawdown_high': high_val + starting_balance, }) - csum_min, csum_max = calculate_csum(results) + csum_min, csum_max = calculate_csum(results, starting_balance) strat_stats.update({ 'csum_min': csum_min, 'csum_max': csum_max @@ -493,7 +493,15 @@ def text_table_add_metrics(strat_results: Dict) -> str: return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl") else: - return '' + start_balance = round_coin_value(strat_results['starting_balance'], + strat_results['stake_currency']) + stake_amount = round_coin_value(strat_results['stake_amount'], + strat_results['stake_currency']) + message = ("No trades made. " + f"Your starting balance was {start_balance}, " + f"and your stake was {stake_amount}." + ) + return message def show_backtest_results(config: Dict, backtest_stats: Dict): From 37d7d2afd5c953c413024964d0078172ba1e3e1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 19:50:10 +0100 Subject: [PATCH 426/563] Wallets should not recalculate close_profit for closed trades --- freqtrade/wallets.py | 2 +- tests/conftest_trades.py | 2 ++ tests/test_freqtradebot.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f5ce4c102..c2085641e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -69,7 +69,7 @@ class Wallets: _wallets = {} closed_trades = Trade.get_trades_proxy(is_open=False) open_trades = Trade.get_trades_proxy(is_open=True) - tot_profit = sum([trade.calc_profit() for trade in closed_trades]) + tot_profit = sum([trade.close_profit_abs for trade in closed_trades]) tot_in_trades = sum([trade.stake_amount for trade in open_trades]) current_stake = self.start_cap + tot_profit - tot_in_trades diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 6a42d04e3..025aac1b6 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -82,6 +82,7 @@ def mock_trade_2(fee): open_rate=0.123, close_rate=0.128, close_profit=0.005, + close_profit_abs=0.000584127, exchange='bittrex', is_open=False, open_order_id='dry_run_sell_12345', @@ -141,6 +142,7 @@ def mock_trade_3(fee): open_rate=0.05, close_rate=0.06, close_profit=0.01, + close_profit_abs=0.000155, exchange='bittrex', is_open=False, strategy='DefaultStrategy', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3bd2f5607..d7d2e19f6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2243,6 +2243,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime + open_trade.close_profit_abs = 0.001 open_trade.is_open = False Trade.session.add(open_trade) @@ -2290,6 +2291,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime + open_trade.close_profit_abs = 0.001 open_trade.is_open = False Trade.session.add(open_trade) From 7913166453518733fcd793879d0150d1339796d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 20:07:27 +0100 Subject: [PATCH 427/563] Improve performance by updating wallets only when necessary --- freqtrade/optimize/backtesting.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c60cfa9b7..f921f64c3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -180,10 +180,6 @@ class Backtesting: PairLocks.reset_locks() Trade.reset_trades() - def update_wallets(self): - if self.wallets: - self.wallets.update() - def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ Helper function to convert a processed dataframes into lists for performance reasons. @@ -272,7 +268,6 @@ class Backtesting: def _enter_trade(self, pair: str, row, max_open_trades: int, open_trade_count: int) -> Optional[Trade]: - self.update_wallets() try: stake_amount = self.wallets.get_trade_stake_amount( pair, max_open_trades - open_trade_count, None) @@ -391,7 +386,6 @@ class Backtesting: trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured if trade_entry: - self.update_wallets() # logger.debug(f"{pair} - Backtesting sell {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) @@ -404,7 +398,7 @@ class Backtesting: tmp += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) - self.update_wallets() + self.wallets.update() return trade_list_to_dataframe(trades) From f04f07299c7689841eaf7eab15c574c09c5774b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 20:19:03 +0100 Subject: [PATCH 428/563] Improve backtesting metrics --- docs/backtesting.md | 39 ++++++++++++++++++-------- freqtrade/optimize/optimize_reports.py | 36 +++++++++++++----------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index ada788da9..bac12dae0 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -252,11 +252,12 @@ A backtesting result will look like that: | Max open trades | 3 | | | | | Total trades | 429 | -| Starting capital | 0.01000000 BTC | -| End capital | 0.01762792 BTC | +| Starting balance | 0.01000000 BTC | +| Final balance | 0.01762792 BTC | | Absolute profit | 0.00762792 BTC | -| Total Profit % | 76.2% | +| Total profit % | 76.2% | | Trades per day | 3.575 | +| Avg. stake amount | 0.001 | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -269,7 +270,12 @@ A backtesting result will look like that: | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | -| Max Drawdown | 50.63% | +| Min balance | 0.00945123 BTC | +| Max balance | 0.01846651 BTC | +| Drawdown | 50.63% | +| Drawdown | 0.0015 BTC | +| Drawdown high | 0.0013 BTC | +| Drawdown low | -0.0002 BTC | | Drawdown Start | 2019-02-15 14:10:00 | | Drawdown End | 2019-04-11 18:15:00 | | Market change | -5.88% | @@ -333,11 +339,12 @@ It contains some useful key metrics about performance of your strategy on backte | Max open trades | 3 | | | | | Total trades | 429 | -| Starting capital | 0.01000000 BTC | -| End capital | 0.01762792 BTC | +| Starting balance | 0.01000000 BTC | +| Final balance | 0.01762792 BTC | | Absolute profit | 0.00762792 BTC | -| Total Profit % | 76.2% | +| Total profit % | 76.2% | | Trades per day | 3.575 | +| Avg. stake amount | 0.001 | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -350,7 +357,12 @@ It contains some useful key metrics about performance of your strategy on backte | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | -| Max Drawdown | 50.63% | +| Min balance | 0.00945123 BTC | +| Max balance | 0.01846651 BTC | +| Drawdown | 50.63% | +| Drawdown | 0.0015 BTC | +| Drawdown high | 0.0013 BTC | +| Drawdown low | -0.0002 BTC | | Drawdown Start | 2019-02-15 14:10:00 | | Drawdown End | 2019-04-11 18:15:00 | | Market change | -5.88% | @@ -361,18 +373,21 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). - `Total trades`: Identical to the total trades of the backtest output table. -- `Starting capital`: Start capital - as given by dry-run-wallet (config or command line). -- `End capital`: Final capital - starting capital + absolute profit. +- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line). +- `End balance`: Final balance - starting balance + absolute profit. - `Absolute profit`: Profit made in stake currency. -- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. +- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). +- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. -- `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). +- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. +- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). +- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. - `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 1ac0ae1d6..cee0bb1ce 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -278,6 +278,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'total_volume': results['stake_amount'].sum(), + 'avg_stake_amount': results['stake_amount'].mean(), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), @@ -295,6 +296,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], + 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), 'starting_balance': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], @@ -334,8 +336,8 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, - 'max_drawdown_low': low_val + starting_balance, - 'max_drawdown_high': high_val + starting_balance, + 'max_drawdown_low': low_val, + 'max_drawdown_high': high_val, }) csum_min, csum_max = calculate_csum(results, starting_balance) @@ -446,14 +448,16 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), - ('Starting capital', round_coin_value(strat_results['starting_balance'], + ('Starting balance', round_coin_value(strat_results['starting_balance'], strat_results['stake_currency'])), - ('End capital', round_coin_value(strat_results['final_balance'], - strat_results['stake_currency'])), + ('Final balance', round_coin_value(strat_results['final_balance'], + strat_results['stake_currency'])), ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), - ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), + ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), + ('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'], + strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], strat_results['stake_currency'])), @@ -474,18 +478,18 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('', ''), # Empty line to improve readability - ('Abs Profit Min', round_coin_value(strat_results['csum_min'], - strat_results['stake_currency'])), - ('Abs Profit Max', round_coin_value(strat_results['csum_max'], - strat_results['stake_currency'])), + ('Min balance', round_coin_value(strat_results['csum_min'], + strat_results['stake_currency'])), + ('Max balance', round_coin_value(strat_results['csum_max'], + strat_results['stake_currency'])), - ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), - ('Max Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + ('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), + ('Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + strat_results['stake_currency'])), + ('Drawdown high', round_coin_value(strat_results['max_drawdown_high'], + strat_results['stake_currency'])), + ('Drawdown low', round_coin_value(strat_results['max_drawdown_low'], strat_results['stake_currency'])), - ('Max Drawdown high', round_coin_value(strat_results['max_drawdown_high'], - strat_results['stake_currency'])), - ('Max Drawdown low', round_coin_value(strat_results['max_drawdown_low'], - strat_results['stake_currency'])), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), From 52acacbed5b43f5cec2f07af74336998c3e51523 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 07:20:51 +0100 Subject: [PATCH 429/563] Check min-trade-stake in backtesting --- freqtrade/optimize/backtesting.py | 4 +++- tests/optimize/test_backtest_detail.py | 3 ++- tests/optimize/test_backtesting.py | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f921f64c3..bd185234f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -273,7 +273,9 @@ class Backtesting: pair, max_open_trades - open_trade_count, None) except DependencyException: stake_amount = 0 - if stake_amount: + min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) + if stake_amount and stake_amount > min_stake_amount: + # print(f"{pair}, {stake_amount}") # Enter trade trade = Trade( pair=pair, diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 4d6605b9f..a56e024f7 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -489,7 +489,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset default_conf["ask_strategy"] = {"use_sell_signal": data.use_sell_signal} - mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0)) + mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 8fba8724b..eda8aac9d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -450,6 +450,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) backtesting = Backtesting(default_conf) pair = 'UNITTEST/BTC' @@ -510,6 +511,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) backtesting = Backtesting(default_conf) @@ -555,6 +557,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad default_conf['enable_protections'] = True mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) tests = [ ['sine', 9], ['raise', 10], @@ -586,6 +589,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, default_conf['protections'] = protections default_conf['enable_protections'] = True + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results @@ -623,6 +627,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC', datadir=testdatadir) @@ -655,6 +660,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) return dataframe + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) From 394a6bbf2a86c8c4990c1f38e566ebaaa2ea2561 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 20:21:30 +0100 Subject: [PATCH 430/563] Fix some type errors --- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/optimize/optimize_reports.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bd185234f..7028a38cd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -274,7 +274,7 @@ class Backtesting: except DependencyException: stake_amount = 0 min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) - if stake_amount and stake_amount > min_stake_amount: + if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # print(f"{pair}, {stake_amount}") # Enter trade trade = Trade( @@ -341,7 +341,7 @@ class Backtesting: indexes: Dict = {} tmp = start_date + timedelta(minutes=self.timeframe_min) - open_trades: Dict[str, List] = defaultdict(list) + open_trades: Dict[str, List[Trade]] = defaultdict(list) open_trade_count = 0 # Loop timerange and get candle for each pair at that point in time diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index cee0bb1ce..e7111f20c 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -479,9 +479,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], - strat_results['stake_currency'])), + strat_results['stake_currency'])), ('Max balance', round_coin_value(strat_results['csum_max'], - strat_results['stake_currency'])), + strat_results['stake_currency'])), ('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), ('Drawdown', round_coin_value(strat_results['max_drawdown_abs'], From 03eb23a4ce7cdf54ffbc3595814ca2029f979262 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 19:29:04 +0100 Subject: [PATCH 431/563] 2 levels of Trade models, one with and one without sqlalchemy Fixes a performance issue when backtesting with sqlalchemy, as that uses descriptors for all properties. --- freqtrade/optimize/backtesting.py | 11 +- freqtrade/persistence/__init__.py | 3 +- freqtrade/persistence/models.py | 244 ++++++++++++++++++++---------- 3 files changed, 170 insertions(+), 88 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7028a38cd..322a3f00b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -23,6 +23,7 @@ from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.persistence import PairLocks, Trade +from freqtrade.persistence.models import LocalTrade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -267,7 +268,7 @@ class Backtesting: return None def _enter_trade(self, pair: str, row, max_open_trades: int, - open_trade_count: int) -> Optional[Trade]: + open_trade_count: int) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount( pair, max_open_trades - open_trade_count, None) @@ -277,7 +278,7 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # print(f"{pair}, {stake_amount}") # Enter trade - trade = Trade( + trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], open_date=row[DATE_IDX], @@ -291,8 +292,8 @@ class Backtesting: return trade return None - def handle_left_open(self, open_trades: Dict[str, List[Trade]], - data: Dict[str, List[Tuple]]) -> List[Trade]: + def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]], + data: Dict[str, List[Tuple]]) -> List[LocalTrade]: """ Handling of left open trades at the end of backtesting """ @@ -381,7 +382,7 @@ class Backtesting: open_trade_count += 1 # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") open_trades[pair].append(trade) - Trade.trades.append(trade) + LocalTrade.trades.append(trade) for trade in open_trades[pair]: # also check the buying candle for sell conditions. diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 35f2bc406..d1fcac0ba 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db +from freqtrade.persistence.models import (LocalTrade, Order, Trade, clean_dry_run_db, cleanup_db, + init_db) from freqtrade.persistence.pairlock_middleware import PairLocks diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f72705c34..48ae8bb40 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -199,67 +199,67 @@ class Order(_DECL_BASE): return Order.query.filter(Order.ft_is_open.is_(True)).all() -class Trade(_DECL_BASE): +class LocalTrade(): """ Trade database model. - Also handles updating and querying trades + Used in backtesting - must be aligned to Trade model! + """ - __tablename__ = 'trades' - - use_db: bool = True + use_db: bool = False # Trades container for backtesting - trades: List['Trade'] = [] + trades: List['LocalTrade'] = [] - id = Column(Integer, primary_key=True) + id: int = 0 - orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") + orders: List[Order] = [] - exchange = Column(String, nullable=False) - pair = Column(String, nullable=False, index=True) - is_open = Column(Boolean, nullable=False, default=True, index=True) - fee_open = Column(Float, nullable=False, default=0.0) - fee_open_cost = Column(Float, nullable=True) - fee_open_currency = Column(String, nullable=True) - fee_close = Column(Float, nullable=False, default=0.0) - fee_close_cost = Column(Float, nullable=True) - fee_close_currency = Column(String, nullable=True) - open_rate = Column(Float) - open_rate_requested = Column(Float) + exchange: str = '' + pair: str = '' + is_open: bool = True + fee_open: float = 0.0 + fee_open_cost: Optional[float] = None + fee_open_currency: str = '' + fee_close: float = 0.0 + fee_close_cost: Optional[float] = None + fee_close_currency: str = '' + open_rate: float + open_rate_requested: Optional[float] = None # open_trade_value - calculated via _calc_open_trade_value - open_trade_value = Column(Float) - close_rate = Column(Float) - close_rate_requested = Column(Float) - close_profit = Column(Float) - close_profit_abs = Column(Float) - stake_amount = Column(Float, nullable=False) - amount = Column(Float) - amount_requested = Column(Float) - open_date = Column(DateTime, nullable=False, default=datetime.utcnow) - close_date = Column(DateTime) - open_order_id = Column(String) + open_trade_value: float + close_rate: Optional[float] = None + close_rate_requested: Optional[float] = None + close_profit: Optional[float] = None + close_profit_abs: Optional[float] = None + stake_amount: float + amount: float + amount_requested: Optional[float] = None + open_date: datetime + close_date: Optional[datetime] = None + open_order_id: Optional[str] = None # absolute value of the stop loss - stop_loss = Column(Float, nullable=True, default=0.0) + stop_loss: float = 0.0 # percentage value of the stop loss - stop_loss_pct = Column(Float, nullable=True) + stop_loss_pct: float = 0.0 # absolute value of the initial stop loss - initial_stop_loss = Column(Float, nullable=True, default=0.0) + initial_stop_loss: float = 0.0 # percentage value of the initial stop loss - initial_stop_loss_pct = Column(Float, nullable=True) + initial_stop_loss_pct: float = 0.0 # stoploss order id which is on exchange - stoploss_order_id = Column(String, nullable=True, index=True) + stoploss_order_id: Optional[str] = None # last update time of the stoploss order on exchange - stoploss_last_update = Column(DateTime, nullable=True) + stoploss_last_update: Optional[datetime] = None # absolute value of the highest reached price - max_rate = Column(Float, nullable=True, default=0.0) + max_rate: float = 0.0 # Lowest price reached - min_rate = Column(Float, nullable=True) - sell_reason = Column(String, nullable=True) - sell_order_status = Column(String, nullable=True) - strategy = Column(String, nullable=True) - timeframe = Column(Integer, nullable=True) + min_rate: float = 0.0 + sell_reason: str = '' + sell_order_status: str = '' + strategy: str = '' + timeframe: Optional[int] = None def __init__(self, **kwargs): - super().__init__(**kwargs) + for key in kwargs: + setattr(self, key, kwargs[key]) self.recalc_open_trade_value() def __repr__(self): @@ -349,8 +349,7 @@ class Trade(_DECL_BASE): """ Resets all trades. Only active for backtesting mode. """ - if not Trade.use_db: - Trade.trades = [] + LocalTrade.trades = [] def adjust_min_max_rates(self, current_price: float) -> None: """ @@ -418,8 +417,8 @@ class Trade(_DECL_BASE): if order_type in ('market', 'limit') and order['side'] == 'buy': # Update open rate and actual amount - self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) - self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) + self.open_rate = float(safe_value_fallback(order, 'average', 'price')) + self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_value() if self.is_open: logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') @@ -443,7 +442,7 @@ class Trade(_DECL_BASE): Sets close_rate to the given rate, calculates total profit and marks trade as closed """ - self.close_rate = Decimal(rate) + self.close_rate = rate self.close_profit = self.calc_profit_ratio() self.close_profit_abs = self.calc_profit() self.close_date = self.close_date or datetime.utcnow() @@ -488,14 +487,6 @@ class Trade(_DECL_BASE): def update_order(self, order: Dict) -> None: Order.update_orders(self.orders, order) - def delete(self) -> None: - - for order in self.orders: - Order.session.delete(order) - - Trade.session.delete(self) - Trade.session.flush() - def _calc_open_trade_value(self) -> float: """ Calculate the open_rate including open_fee. @@ -525,7 +516,7 @@ class Trade(_DECL_BASE): if rate is None and not self.close_rate: return 0.0 - sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) + sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore fees = sell_trade * Decimal(fee or self.fee_close) return float(sell_trade - fees) @@ -597,7 +588,7 @@ class Trade(_DECL_BASE): @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, - ) -> List['Trade']: + ) -> List['LocalTrade']: """ Helper function to query Trades. Returns a List of trades, filtered on the parameters given. @@ -606,30 +597,19 @@ class Trade(_DECL_BASE): :return: unsorted List[Trade] """ - if Trade.use_db: - trade_filter = [] - if pair: - trade_filter.append(Trade.pair == pair) - if open_date: - trade_filter.append(Trade.open_date > open_date) - if close_date: - trade_filter.append(Trade.close_date > close_date) - if is_open is not None: - trade_filter.append(Trade.is_open.is_(is_open)) - return Trade.get_trades(trade_filter).all() - else: - # Offline mode - without database - sel_trades = [trade for trade in Trade.trades] - if pair: - sel_trades = [trade for trade in sel_trades if trade.pair == pair] - if open_date: - sel_trades = [trade for trade in sel_trades if trade.open_date > open_date] - if close_date: - sel_trades = [trade for trade in sel_trades if trade.close_date - and trade.close_date > close_date] - if is_open is not None: - sel_trades = [trade for trade in sel_trades if trade.is_open == is_open] - return sel_trades + + # Offline mode - without database + sel_trades = [trade for trade in LocalTrade.trades] + if pair: + sel_trades = [trade for trade in sel_trades if trade.pair == pair] + if open_date: + sel_trades = [trade for trade in sel_trades if trade.open_date > open_date] + if close_date: + sel_trades = [trade for trade in sel_trades if trade.close_date + and trade.close_date > close_date] + if is_open is not None: + sel_trades = [trade for trade in sel_trades if trade.is_open == is_open] + return sel_trades @staticmethod def get_open_trades() -> List[Any]: @@ -735,6 +715,106 @@ class Trade(_DECL_BASE): logger.info(f"New stoploss: {trade.stop_loss}.") +class Trade(_DECL_BASE, LocalTrade): + """ + Trade database model. + Also handles updating and querying trades + """ + __tablename__ = 'trades' + + use_db: bool = True + + id = Column(Integer, primary_key=True) + + orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") + + exchange = Column(String, nullable=False) + pair = Column(String, nullable=False, index=True) + is_open = Column(Boolean, nullable=False, default=True, index=True) + fee_open = Column(Float, nullable=False, default=0.0) + fee_open_cost = Column(Float, nullable=True) + fee_open_currency = Column(String, nullable=True) + fee_close = Column(Float, nullable=False, default=0.0) + fee_close_cost = Column(Float, nullable=True) + fee_close_currency = Column(String, nullable=True) + open_rate = Column(Float) + open_rate_requested = Column(Float) + # open_trade_value - calculated via _calc_open_trade_value + open_trade_value = Column(Float) + close_rate = Column(Float) + close_rate_requested = Column(Float) + close_profit = Column(Float) + close_profit_abs = Column(Float) + stake_amount = Column(Float, nullable=False) + amount = Column(Float) + amount_requested = Column(Float) + open_date = Column(DateTime, nullable=False, default=datetime.utcnow) + close_date = Column(DateTime) + open_order_id = Column(String) + # absolute value of the stop loss + stop_loss = Column(Float, nullable=True, default=0.0) + # percentage value of the stop loss + stop_loss_pct = Column(Float, nullable=True) + # absolute value of the initial stop loss + initial_stop_loss = Column(Float, nullable=True, default=0.0) + # percentage value of the initial stop loss + initial_stop_loss_pct = Column(Float, nullable=True) + # stoploss order id which is on exchange + stoploss_order_id = Column(String, nullable=True, index=True) + # last update time of the stoploss order on exchange + stoploss_last_update = Column(DateTime, nullable=True) + # absolute value of the highest reached price + max_rate = Column(Float, nullable=True, default=0.0) + # Lowest price reached + min_rate = Column(Float, nullable=True) + sell_reason = Column(String, nullable=True) + sell_order_status = Column(String, nullable=True) + strategy = Column(String, nullable=True) + timeframe = Column(Integer, nullable=True) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.recalc_open_trade_value() + + def delete(self) -> None: + + for order in self.orders: + Order.session.delete(order) + + Trade.session.delete(self) + Trade.session.flush() + + @staticmethod + def get_trades_proxy(*, pair: str = None, is_open: bool = None, + open_date: datetime = None, close_date: datetime = None, + ) -> List['LocalTrade']: + """ + Helper function to query Trades. + Returns a List of trades, filtered on the parameters given. + In live mode, converts the filter to a database query and returns all rows + In Backtest mode, uses filters on Trade.trades to get the result. + + :return: unsorted List[Trade] + """ + if Trade.use_db: + trade_filter = [] + if pair: + trade_filter.append(Trade.pair == pair) + if open_date: + trade_filter.append(Trade.open_date > open_date) + if close_date: + trade_filter.append(Trade.close_date > close_date) + if is_open is not None: + trade_filter.append(Trade.is_open.is_(is_open)) + return Trade.get_trades(trade_filter).all() + else: + return LocalTrade.get_trades_proxy( + pair=pair, is_open=is_open, + open_date=open_date, + close_date=close_date + ) + + class PairLock(_DECL_BASE): """ Pair Locks database model. From 53a57f2c81f05c6bf7f2ce3cf5bd5cc95d591464 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 20:22:00 +0100 Subject: [PATCH 432/563] Change some types Fix types of new model object --- freqtrade/data/btanalysis.py | 4 ++-- freqtrade/optimize/backtesting.py | 12 ++++++------ freqtrade/plugins/protections/cooldown_period.py | 3 ++- freqtrade/plugins/protections/iprotection.py | 6 +++--- freqtrade/plugins/protections/low_profit_pairs.py | 2 +- freqtrade/plugins/protections/stoploss_guard.py | 2 +- tests/conftest.py | 4 ++-- tests/optimize/test_backtest_detail.py | 1 - 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 3adee8775..c98477f4e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -10,7 +10,7 @@ import pandas as pd from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.misc import json_load -from freqtrade.persistence import Trade, init_db +from freqtrade.persistence import LocalTrade, Trade, init_db logger = logging.getLogger(__name__) @@ -224,7 +224,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str, return df_final[df_final['open_trades'] > max_open_trades] -def trade_list_to_dataframe(trades: List[Trade]) -> pd.DataFrame: +def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame: """ Convert list of Trade objects to pandas Dataframe :param trades: List of trade objects diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 322a3f00b..aeafaffd3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -211,7 +211,7 @@ class Backtesting: data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)] return data - def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple, + def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, trade_dur: int) -> float: """ Get close rate for backtesting result @@ -251,10 +251,10 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[Trade]: + def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX], - sell_row[BUY_IDX], sell_row[SELL_IDX], + sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore + sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: @@ -331,7 +331,7 @@ class Backtesting: :param enable_protections: Should protections be enabled? :return: DataFrame with trades (results of backtesting) """ - trades: List[Trade] = [] + trades: List[LocalTrade] = [] self.prepare_backtest(enable_protections) # Use dict of lists with data for performance @@ -342,7 +342,7 @@ class Backtesting: indexes: Dict = {} tmp = start_date + timedelta(minutes=self.timeframe_min) - open_trades: Dict[str, List[Trade]] = defaultdict(list) + open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) open_trade_count = 0 # Loop timerange and get candle for each pair at that point in time diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 2d7d7b4c7..f74f83885 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -44,7 +44,8 @@ class CooldownPeriod(IProtection): trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) if trades: # Get latest trade - trade = sorted(trades, key=lambda t: t.close_date)[-1] + # Ignore type error as we know we only get closed trades. + trade = sorted(trades, key=lambda t: t.close_date)[-1] # type: ignore self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info) until = self.calculate_lock_end([trade], self._stop_duration) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 684bf6cd3..d034beefc 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import plural from freqtrade.mixins import LoggingMixin -from freqtrade.persistence import Trade +from freqtrade.persistence import LocalTrade logger = logging.getLogger(__name__) @@ -93,11 +93,11 @@ class IProtection(LoggingMixin, ABC): """ @staticmethod - def calculate_lock_end(trades: List[Trade], stop_minutes: int) -> datetime: + def calculate_lock_end(trades: List[LocalTrade], stop_minutes: int) -> datetime: """ Get lock end time """ - max_date: datetime = max([trade.close_date for trade in trades]) + max_date: datetime = max([trade.close_date for trade in trades if trade.close_date]) # comming from Database, tzinfo is not set. if max_date.tzinfo is None: max_date = max_date.replace(tzinfo=timezone.utc) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 9d5ed35b4..7822ce73c 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -53,7 +53,7 @@ class LowProfitPairs(IProtection): # Not enough trades in the relevant period return False, None, None - profit = sum(trade.close_profit for trade in trades) + profit = sum(trade.close_profit for trade in trades if trade.close_profit) if profit < self._required_profit: self.log_once( f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 5a9b9ddd0..635c0be04 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -56,7 +56,7 @@ class StoplossGuard(IProtection): trades = [trade for trade in trades1 if (str(trade.sell_reason) in ( SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, SellType.STOPLOSS_ON_EXCHANGE.value) - and trade.close_profit < 0)] + and trade.close_profit and trade.close_profit < 0)] if len(trades) < self._trade_limit: return False, None, None diff --git a/tests/conftest.py b/tests/conftest.py index 6e70603b1..793ba83b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Trade, init_db +from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, @@ -191,7 +191,7 @@ def create_mock_trades(fee, use_db: bool = True): if use_db: Trade.session.add(trade) else: - Trade.trades.append(trade) + LocalTrade.trades.append(trade) # Simulate dry_run entries trade = mock_trade_1(fee) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index a56e024f7..0ba6f4a7f 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument import logging -from unittest.mock import MagicMock import pytest From 60db6ccf454715aa9d8b2ba56e4676006e8fb1fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 20:07:00 +0100 Subject: [PATCH 433/563] Add test for subclassing --- freqtrade/persistence/models.py | 8 ++++---- tests/test_persistence.py | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 48ae8bb40..51a48c246 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -222,16 +222,16 @@ class LocalTrade(): fee_close: float = 0.0 fee_close_cost: Optional[float] = None fee_close_currency: str = '' - open_rate: float + open_rate: float = 0.0 open_rate_requested: Optional[float] = None # open_trade_value - calculated via _calc_open_trade_value - open_trade_value: float + open_trade_value: float = 0.0 close_rate: Optional[float] = None close_rate_requested: Optional[float] = None close_profit: Optional[float] = None close_profit_abs: Optional[float] = None - stake_amount: float - amount: float + stake_amount: float = 0.0 + amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime close_date: Optional[datetime] = None diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1fced3e16..18a377ca3 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,14 +1,16 @@ # pragma pylint: disable=missing-docstring, C0103 +from types import FunctionType import logging from unittest.mock import MagicMock import arrow import pytest from sqlalchemy import create_engine +from sqlalchemy.sql.schema import Column from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db +from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, log_has, log_has_re @@ -1176,3 +1178,25 @@ def test_select_order(fee): assert order.ft_order_side == 'stoploss' order = trades[4].select_order('sell', False) assert order is None + + +def test_Trade_object_idem(): + + assert issubclass(Trade, LocalTrade) + + trade = vars(Trade) + localtrade = vars(LocalTrade) + + # Parent (LocalTrade) should have the same attributes + for item in trade: + # Exclude private attributes and open_date (as it's not assigned a default) + if (not item.startswith('_') + and item not in ('delete', 'session', 'query', 'open_date')): + assert item in localtrade + + # Fails if only a column is added without corresponding parent field + for item in localtrade: + if (not item.startswith('__') + and item not in ('trades', ) + and type(getattr(LocalTrade, item)) not in (property, FunctionType)): + assert item in trade From fc256749af4a29ee30353a2ae4edc17f9b3a4021 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Feb 2021 06:54:33 +0100 Subject: [PATCH 434/563] Add test for backtesting _enter_trade --- freqtrade/optimize/backtesting.py | 7 +++-- tests/data/test_btanalysis.py | 1 - tests/optimize/test_backtesting.py | 41 +++++++++++++++++++++++++++++- tests/test_persistence.py | 3 +-- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index aeafaffd3..9a4a3787a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -22,8 +22,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) -from freqtrade.persistence import PairLocks, Trade -from freqtrade.persistence.models import LocalTrade +from freqtrade.persistence import LocalTrade, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -267,13 +266,13 @@ class Backtesting: return None - def _enter_trade(self, pair: str, row, max_open_trades: int, + def _enter_trade(self, pair: str, row: List, max_open_trades: int, open_trade_count: int) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount( pair, max_open_trades - open_trade_count, None) except DependencyException: - stake_amount = 0 + return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # print(f"{pair}, {stake_amount}") diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 538c89a90..e42c13e18 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -301,7 +301,6 @@ def test_calculate_csum(testdatadir): assert csum_min1 == csum_min + 5 assert csum_max1 == csum_max + 5 - with pytest.raises(ValueError, match='Trade dataframe empty.'): csum_min, csum_max = calculate_csum(DataFrame()) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index eda8aac9d..354b3f6b0 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -17,8 +17,9 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize.backtesting import Backtesting +from freqtrade.persistence import LocalTrade from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType @@ -447,6 +448,44 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) +def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: + default_conf['ask_strategy']['use_sell_signal'] = False + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) + patch_exchange(mocker) + default_conf['stake_amount'] = 'unlimited' + backtesting = Backtesting(default_conf) + pair = 'UNITTEST/BTC' + row = [ + pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), + 1, # Sell + 0.001, # Open + 0.0011, # Close + 0, # Sell + 0.00099, # Low + 0.0012, # High + ] + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + assert isinstance(trade, LocalTrade) + assert trade.stake_amount == 495 + + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2) + assert trade is None + + # Stake-amount too high! + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) + + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + assert trade is None + + # Stake-amount too high! + mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", + side_effect=DependencyException) + + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + assert trade is None + + def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 18a377ca3..1a8124b00 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,12 +1,11 @@ # pragma pylint: disable=missing-docstring, C0103 -from types import FunctionType import logging +from types import FunctionType from unittest.mock import MagicMock import arrow import pytest from sqlalchemy import create_engine -from sqlalchemy.sql.schema import Column from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException From d3fb473e578e0f1ea5b7275d7023e6f8088d2583 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Feb 2021 20:21:50 +0100 Subject: [PATCH 435/563] Improve backtesting documentation --- docs/backtesting.md | 86 ++++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index bac12dae0..9fa9025d8 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -95,8 +95,7 @@ Strategy arguments: ## Test your strategy with Backtesting Now you have good Buy and Sell strategies and some historic data, you want to test it against -real data. This is what we call -[backtesting](https://en.wikipedia.org/wiki/Backtesting). +real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/` by default. If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`. @@ -104,6 +103,8 @@ For details on downloading, please refer to the [Data Downloading](data-download The result of backtesting will confirm if your bot has better odds of making a profit than a loss. +All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation. + !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. @@ -111,38 +112,46 @@ The result of backtesting will confirm if your bot has better odds of making a p To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. -### Run a backtesting against the currencies listed in your config file +### Example backtesting commands -#### With 5 min candle (OHLCV) data (per default) +With 5 min candle (OHLCV) data (per default) ```bash -freqtrade backtesting +freqtrade backtesting --strategy AwesomeStrategy ``` -#### With 1 min candle (OHLCV) data +Where `--strategy AwesomeStrategy` / `-s AwesomeStrategy` refers to the class name of the strategy, which is within a python file in the `user_data/strategies` directory. + +--- + +With 1 min candle (OHLCV) data ```bash -freqtrade backtesting --timeframe 1m +freqtrade backtesting --strategy AwesomeStrategy --timeframe 1m ``` -#### Using a different on-disk historical candle (OHLCV) data source +--- + +Providing a custom starting balance of 1000 (in stake currency) + +```bash +freqtrade backtesting --strategy AwesomeStrategy --dry-run-wallet 1000 +``` + +--- + +Using a different on-disk historical candle (OHLCV) data source Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory. You can then use this data for backtesting as follows: ```bash -freqtrade --datadir user_data/data/bittrex-20180101 backtesting +freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/bittrex-20180101 ``` -#### With a (custom) strategy file +--- -```bash -freqtrade backtesting -s SampleStrategy -``` - -Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory. - -#### Comparing multiple Strategies +Comparing multiple Strategies ```bash freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timeframe 5m @@ -150,23 +159,29 @@ freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timefram Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies. -#### Exporting trades to file +--- + +Exporting trades to file ```bash -freqtrade backtesting --export trades --config config.json --strategy SampleStrategy +freqtrade backtesting --strategy backtesting --export trades --config config.json ``` The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. -#### Exporting trades to file specifying a custom filename +--- + +Exporting trades to file specifying a custom filename ```bash -freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json +freqtrade backtesting --strategy backtesting --export trades --export-filename=backtest_samplestrategy.json ``` Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period). -#### Supplying custom fee value +--- + +Supplying custom fee value Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt. To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting. @@ -181,26 +196,26 @@ freqtrade backtesting --fee 0.001 !!! Note Only supply this option (or the corresponding configuration parameter) if you want to experiment with different fee values. By default, Backtesting fetches the default fee from the exchange pair/market info. -#### Running backtest with smaller testset by using timerange +--- -Use the `--timerange` argument to change how much of the testset you want to use. +Running backtest with smaller test-set by using timerange +Use the `--timerange` argument to change how much of the test-set you want to use. -For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your inputdata. +For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your input data. ```bash freqtrade backtesting --timerange=20190501- ``` -You can also specify particular dates or a range span indexed by start and stop. +You can also specify particular date ranges. The full timerange specification: -- Use tickframes till 2018/01/31: `--timerange=-20180131` -- Use tickframes since 2018/01/31: `--timerange=20180131-` -- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` -- Use tickframes between POSIX timestamps 1527595200 1527618600: - `--timerange=1527595200-1527618600` +- Use data until 2018/01/31: `--timerange=-20180131` +- Use data since 2018/01/31: `--timerange=20180131-` +- Use data since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` +- Use data between POSIX / epoch timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600` ## Understand the backtesting result @@ -296,9 +311,9 @@ here: The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC. -The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums up all the profits/losses. -The column `tot profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`). -In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `tot_profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`. +The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses. +The column `Tot Profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`). +In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `Tot Profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`. Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set. @@ -452,6 +467,5 @@ Detailed output for all strategies one after the other will be available, so mak ## Next step -Great, your strategy is profitable. What if the bot can give your the -optimal parameters to use for your strategy? +Great, your strategy is profitable. What if the bot can give your the optimal parameters to use for your strategy? Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md) From 86f9409fd293604e03408e89beb460078768d103 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Feb 2021 20:14:33 +0100 Subject: [PATCH 436/563] fix --stake-amount parameter --- freqtrade/commands/cli_options.py | 1 - freqtrade/configuration/configuration.py | 7 +++++++ tests/test_configuration.py | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 90ebb5e6a..3b27237da 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -133,7 +133,6 @@ AVAILABLE_CLI_OPTIONS = { "stake_amount": Arg( '--stake-amount', help='Override the value of the `stake_amount` configuration setting.', - type=float, ), # Backtesting "position_stacking": Arg( diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 6295d01d4..88447e490 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -229,6 +229,13 @@ class Configuration: elif config['runmode'] in NON_UTIL_MODES: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) + if self.args.get('stake_amount', None): + # Convert explicitly to float to support CLI argument for both unlimited and value + try: + self.args['stake_amount'] = float(self.args['stake_amount']) + except ValueError: + pass + self._args_to_config(config, argname='stake_amount', logstring='Parameter --stake-amount detected, ' 'overriding stake_amount to: {} ...') diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 94c3e24f6..6b3df392b 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -430,7 +430,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non '--enable-position-stacking', '--disable-max-market-positions', '--timerange', ':100', - '--export', '/bar/foo' + '--export', '/bar/foo', + '--stake-amount', 'unlimited' ] args = Arguments(arglist).get_parsed_arg() @@ -463,6 +464,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert 'export' in config assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog) + assert 'stake_amount' in config + assert config['stake_amount'] == 'unlimited' def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None: From 98f3142b30e2067b4ead4e3dec51848d56a9c0cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Feb 2021 19:48:06 +0100 Subject: [PATCH 437/563] Improve handling of backtesting params --- freqtrade/commands/cli_options.py | 2 +- freqtrade/commands/optimize_commands.py | 11 ++++++++--- freqtrade/configuration/configuration.py | 6 +++--- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtesting.py | 17 +++++++++++++---- tests/optimize/test_hyperopt.py | 17 +++++++++++++---- 6 files changed, 39 insertions(+), 16 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 3b27237da..15c13cec9 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -111,7 +111,7 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', ), "dry_run_wallet": Arg( - '--dry-run-wallet', + '--dry-run-wallet', '--starting-balance', help='Starting balance, used for backtesting / hyperopt and dry-runs.', type=float, ), diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index bf36972c4..130743f68 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -4,6 +4,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException +from freqtrade.misc import round_coin_value from freqtrade.state import RunMode @@ -22,9 +23,13 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ RunMode.BACKTEST: 'backtesting', RunMode.HYPEROPT: 'hyperoptimization', } - if (method in no_unlimited_runmodes.keys() and - config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT and - config['max_open_trades'] != float('inf')): + if method in no_unlimited_runmodes.keys(): + if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT + and config['stake_amount'] > config['dry_run_wallet']): + wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency']) + stake = round_coin_value(config['stake_amount'], config['stake_currency']) + raise OperationalException(f"Starting balance ({wallet}) " + f"is smaller than stake_amount {stake}.") pass # config['dry_run_wallet'] = config['stake_amount'] * \ # config['max_open_trades'] * (2 - config['tradable_balance_ratio']) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 88447e490..a40a4fd83 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -214,9 +214,6 @@ class Configuration: self._args_to_config( config, argname='enable_protections', logstring='Parameter --enable-protections detected, enabling Protections. ...') - # Setting max_open_trades to infinite if -1 - if config.get('max_open_trades') == -1: - config['max_open_trades'] = float('inf') if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: config.update({'use_max_market_positions': False}) @@ -228,6 +225,9 @@ class Configuration: 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) elif config['runmode'] in NON_UTIL_MODES: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) + # Setting max_open_trades to infinite if -1 + if config.get('max_open_trades') == -1: + config['max_open_trades'] = float('inf') if self.args.get('stake_amount', None): # Convert explicitly to float to support CLI argument for both unlimited and value diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9a4a3787a..13ffc1d25 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -128,7 +128,7 @@ class Backtesting: PairLocks.use_db = True Trade.use_db = True - def _set_strategy(self, strategy): + def _set_strategy(self, strategy: IStrategy): """ Load strategy into backtesting """ diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 354b3f6b0..4bbfe8a78 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -9,7 +9,6 @@ import pandas as pd import pytest from arrow import Arrow -from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting from freqtrade.configuration import TimeRange from freqtrade.data import history @@ -232,8 +231,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert log_has('Parameter --fee detected, setting fee to: {} ...'.format(config['fee']), caplog) -def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: - default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT +def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) @@ -241,12 +239,23 @@ def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_con 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', + '--stake-amount', '1', + '--starting-balance', '2' ] - # TODO: does this test still make sense? conf = setup_optimize_configuration(get_args(args), RunMode.BACKTEST) assert isinstance(conf, dict) + args = [ + 'backtesting', + '--config', 'config.json', + '--strategy', 'DefaultStrategy', + '--stake-amount', '1', + '--starting-balance', '0.5' + ] + with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"): + setup_optimize_configuration(get_args(args), RunMode.BACKTEST) + def test_start(mocker, fee, default_conf, caplog) -> None: start_mock = MagicMock() diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 88a4cea2d..9ebdad2b5 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -12,7 +12,6 @@ import pytest from arrow import Arrow from filelock import Timeout -from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException @@ -130,8 +129,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo assert log_has('Parameter --print-all detected ...', caplog) -def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_conf) -> None: - default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT +def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None: patched_configuration_load_config_file(mocker, default_conf) @@ -139,11 +137,22 @@ def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_con 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', + '--stake-amount', '1', + '--starting-balance', '2' ] - # TODO: does this test still make sense? conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) assert isinstance(conf, dict) + args = [ + 'hyperopt', + '--config', 'config.json', + '--strategy', 'DefaultStrategy', + '--stake-amount', '1', + '--starting-balance', '0.5' + ] + with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"): + setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + def test_hyperoptresolver(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) From f5bb5f56f1aeefe13075f418d3ea15f24969fdbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Feb 2021 19:53:29 +0100 Subject: [PATCH 438/563] Update documentation with backtesting compounding possibilities --- docs/backtesting.md | 15 ++++++++++++--- docs/bot-usage.md | 2 +- docs/configuration.md | 7 ++++--- docs/hyperopt.md | 2 +- freqtrade/commands/optimize_commands.py | 6 ------ 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 9fa9025d8..96911763e 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -49,7 +49,7 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections - --dry-run-wallet DRY_RUN_WALLET + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] @@ -108,10 +108,19 @@ All profit calculations include fees, and freqtrade will use the exchange's defa !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. - Please read the [pairlists documentation](plugins.md#pairlists) for more information. - + Please read the [pairlists documentation](plugins.md#pairlists) for more information. To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. +### Starting balance + +Backtesting will require a starting balance, which can be given as `--dry-run-wallet ` or `--starting-balance ` command line argument, or via `dry_run_wallet` configuration setting. +This amount must be higher than `stake_amount`, otherwise the bot will not be able to simulate any trade. + +### Dynamic stake amount + +Backtesting supports [dynamic stake amount](configuration.md#dynamic-stake-amount) by configuring `stake_amount` as `"unlimited"`, which will split the starting balance into `max_open_trades` pieces. +Profits from early trades will result in subsequent higher stake amounts, resulting in compounding of profits over the backtesting period. + ### Example backtesting commands With 5 min candle (OHLCV) data (per default) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4ff6168a0..b65220722 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -67,7 +67,7 @@ optional arguments: --sd-notify Notify systemd service manager. --dry-run Enforce dry-run for trading (removes Exchange secrets and simulates trades). - --dry-run-wallet DRY_RUN_WALLET + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. diff --git a/docs/configuration.md b/docs/configuration.md index 663d9c5b2..2cc22d6ec 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -218,11 +218,12 @@ To allow the bot to trade all the available `stake_currency` in your account (mi "tradable_balance_ratio": 0.99, ``` -!!! Note - This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available). +!!! Tip "Compounding profits" + This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available), and will result in profit compounding. !!! Note "When using Dry-Run Mode" - When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. + When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. + It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. --8<-- "includes/pricing.md" diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ee3d75d0b..d6959b457 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -83,7 +83,7 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections - --dry-run-wallet DRY_RUN_WALLET + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. -e INT, --epochs INT Specify number of epochs (default: 100). diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 130743f68..6323bc2b1 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -30,12 +30,6 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ stake = round_coin_value(config['stake_amount'], config['stake_currency']) raise OperationalException(f"Starting balance ({wallet}) " f"is smaller than stake_amount {stake}.") - pass - # config['dry_run_wallet'] = config['stake_amount'] * \ - # config['max_open_trades'] * (2 - config['tradable_balance_ratio']) - - # logger.warning(f"Changing dry-run-wallet to {config['dry_run_wallet']} " - # "(max_open_trades * stake_amount).") return config From fb489c11c921b77bf6f029c59ecb71f4d8712486 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:07:02 +0100 Subject: [PATCH 439/563] Improve test-coverage of pairlocks --- tests/plugins/test_pairlocks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index dfcbff0ed..fce3a8cd1 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -73,9 +73,13 @@ def test_PairLocks(use_db): assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50)) if use_db: - assert len(PairLock.query.all()) > 0 + locks = PairLocks.get_all_locks() + locks_db = PairLock.query.all() + assert len(locks) == len(locks_db) + assert len(locks_db) > 0 else: # Nothing was pushed to the database + assert len(PairLocks.get_all_locks()) > 0 assert len(PairLock.query.all()) == 0 # Reset use-db variable PairLocks.reset_locks() From f65092459a39ccd7238550a9518f989bab41feb7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:14:25 +0100 Subject: [PATCH 440/563] Fix optimize_reports test --- tests/optimize/test_optimize_reports.py | 32 ++++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index ca6a4ab01..8119c732b 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -102,6 +102,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): # Above sample had no loosing trade assert strat_stats['max_drawdown'] == 0.0 + # Retry with losing trade results = {'DefStrat': { 'results': pd.DataFrame( {"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], @@ -118,18 +119,31 @@ def test_generate_backtest_stats(default_conf, testdatadir): "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.ROI, + SellType.STOP_LOSS, SellType.FORCE_SELL] }), - 'config': default_conf} + 'config': default_conf, + 'locks': [], + 'final_balance': 1000.02, + 'backtest_start_time': Arrow.utcnow().int_timestamp, + 'backtest_end_time': Arrow.utcnow().int_timestamp, + } } - assert strat_stats['max_drawdown'] == 0.0 - assert strat_stats['drawdown_start'] == datetime(1970, 1, 1, tzinfo=timezone.utc) - assert strat_stats['drawdown_end'] == datetime(1970, 1, 1, tzinfo=timezone.utc) - assert strat_stats['drawdown_end_ts'] == 0 - assert strat_stats['drawdown_start_ts'] == 0 + stats = generate_backtest_stats(btdata, results, min_date, max_date) + assert isinstance(stats, dict) + assert 'strategy' in stats + assert 'DefStrat' in stats['strategy'] + assert 'strategy_comparison' in stats + strat_stats = stats['strategy']['DefStrat'] + + assert strat_stats['max_drawdown'] == 0.013803 + assert strat_stats['drawdown_start'] == datetime(2017, 11, 14, 22, 10, tzinfo=timezone.utc) + assert strat_stats['drawdown_end'] == datetime(2017, 11, 14, 22, 43, tzinfo=timezone.utc) + assert strat_stats['drawdown_end_ts'] == 1510699380000 + assert strat_stats['drawdown_start_ts'] == 1510697400000 assert strat_stats['pairlist'] == ['UNITTEST/BTC'] # Test storing stats From 324b9dbdff126f53470919398081b2374d30c8b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:31:21 +0100 Subject: [PATCH 441/563] Simplify wallet code --- freqtrade/optimize/backtesting.py | 3 +-- freqtrade/wallets.py | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 13ffc1d25..b9ae096e2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -115,8 +115,7 @@ class Backtesting: if self.config.get('enable_protections', False): self.protections = ProtectionManager(self.config) - self.wallets = Wallets(self.config, self.exchange) - self.wallets._log = False + self.wallets = Wallets(self.config, self.exchange, log=False) # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index c2085641e..553f7c61d 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -27,15 +27,14 @@ class Wallet(NamedTuple): class Wallets: - def __init__(self, config: dict, exchange: Exchange, skip_update: bool = False) -> None: + def __init__(self, config: dict, exchange: Exchange, log: bool = True) -> None: self._config = config - self._log = True + self._log = log self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self.start_cap = config['dry_run_wallet'] self._last_wallet_refresh = 0 - if not skip_update: - self.update() + self.update() def get_free(self, currency: str) -> float: balance = self._wallets.get(currency) From 6018a0534367bff3895778a50cec945d03d1a0a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:45:22 +0100 Subject: [PATCH 442/563] Improve backtest documentation --- docs/backtesting.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 96911763e..29ddb494b 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -108,12 +108,13 @@ All profit calculations include fees, and freqtrade will use the exchange's defa !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. - Please read the [pairlists documentation](plugins.md#pairlists) for more information. + Please read the [pairlists documentation](plugins.md#pairlists) for more information. + To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. ### Starting balance -Backtesting will require a starting balance, which can be given as `--dry-run-wallet ` or `--starting-balance ` command line argument, or via `dry_run_wallet` configuration setting. +Backtesting will require a starting balance, which can be provided as `--dry-run-wallet ` or `--starting-balance ` command line argument, or via `dry_run_wallet` configuration setting. This amount must be higher than `stake_amount`, otherwise the bot will not be able to simulate any trade. ### Dynamic stake amount @@ -281,7 +282,7 @@ A backtesting result will look like that: | Absolute profit | 0.00762792 BTC | | Total profit % | 76.2% | | Trades per day | 3.575 | -| Avg. stake amount | 0.001 | +| Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -368,7 +369,7 @@ It contains some useful key metrics about performance of your strategy on backte | Absolute profit | 0.00762792 BTC | | Total profit % | 76.2% | | Trades per day | 3.575 | -| Avg. stake amount | 0.001 | +| Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -398,7 +399,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). - `Total trades`: Identical to the total trades of the backtest output table. - `Starting balance`: Start balance - as given by dry-run-wallet (config or command line). -- `End balance`: Final balance - starting balance + absolute profit. +- `Final balance`: Final balance - starting balance + absolute profit. - `Absolute profit`: Profit made in stake currency. - `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). From b2e9295d7f86688e40278ebe253c81b7b6a6450e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 19:57:42 +0100 Subject: [PATCH 443/563] Small stylistic fixes --- freqtrade/optimize/backtesting.py | 1 - freqtrade/persistence/models.py | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b9ae096e2..1b6d2e89c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -274,7 +274,6 @@ class Backtesting: return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): - # print(f"{pair}, {stake_amount}") # Enter trade trade = LocalTrade( pair=pair, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 51a48c246..3a6474696 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -652,9 +652,8 @@ class LocalTrade(): in stake currency """ if Trade.use_db: - total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ - .filter(Trade.is_open.is_(True))\ - .scalar() + total_open_stake_amount = Trade.session.query( + func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() else: total_open_stake_amount = sum( t.stake_amount for t in Trade.get_trades_proxy(is_open=True)) @@ -719,6 +718,8 @@ class Trade(_DECL_BASE, LocalTrade): """ Trade database model. Also handles updating and querying trades + + Note: Fields must be aligned with LocalTrade class """ __tablename__ = 'trades' From d9d5617432cc991a2de976f8e84cb107913ea0d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 20:26:13 +0100 Subject: [PATCH 444/563] UPdate backtesting doc for total profit calc --- docs/backtesting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 29ddb494b..2e91b6e74 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -322,8 +322,8 @@ The bot has made `429` trades for an average duration of `4:12:00`, with a perfo earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC. The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses. -The column `Tot Profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`). -In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `Tot Profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`. +The column `Tot Profit %` shows instead the total profit % in relation to the starting balance. +In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`. Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set. From e791ff60423949ce8bc466370fa223ada47dc6c1 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Sat, 27 Feb 2021 23:28:26 +0100 Subject: [PATCH 445/563] Fix: custom_stoploss returns typo Afaik it should return -0.07 for 7% instead of -0.7. As a side note, really interesting would also be an example for greater than 100% profits. especially when trailing stoploss, like * Once profit is > 200% - stoploss will be set to 150%. I assume it could be as simple as ```py if current_profit > 2: return (-1.50 + current_profit) ```` to achieve it But I'm not quite confident, if the bot can handle stuff smaller than `-1`, since `1` and `-1` seem to have some special meaning and are often used to disable stoploss etc. --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index c051e2232..dcd340fd1 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -176,7 +176,7 @@ class AwesomeStrategy(IStrategy): if current_profit > 0.25: return (-0.15 + current_profit) if current_profit > 0.20: - return (-0.7 + current_profit) + return (-0.07 + current_profit) return 1 ``` From 05f057fe727769a186c8a84b43b2dcc98544d6c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Feb 2021 08:47:56 +0100 Subject: [PATCH 446/563] Stringify favicon path potentially closes #4459 --- freqtrade/rpc/api_server/web_ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 6d7e77953..13d22a63e 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -10,7 +10,7 @@ router_ui = APIRouter() @router_ui.get('/favicon.ico', include_in_schema=False) async def favicon(): - return FileResponse(Path(__file__).parent / 'ui/favicon.ico') + return FileResponse(str(Path(__file__).parent / 'ui/favicon.ico')) @router_ui.get('/{rest_of_path:path}', include_in_schema=False) From 0895407811c9e9926c2939b30cb303d75b5a2bca Mon Sep 17 00:00:00 2001 From: Florian Reitmeir Date: Wed, 17 Feb 2021 23:09:39 +0100 Subject: [PATCH 447/563] add balance_dust_level parameter to make telegram less chatty --- freqtrade/constants.py | 3 ++- freqtrade/rpc/telegram.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 802ddc2b1..51f178806 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -243,7 +243,8 @@ CONF_SCHEMA = { } } }, - 'required': ['enabled', 'token', 'chat_id'] + 'required': ['enabled', 'token', 'chat_id'], + 'balance_dust_level': 0.0001 }, 'webhook': { 'type': 'object', diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 88019601c..ad3a00292 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -487,6 +487,8 @@ class Telegram(RPCHandler): result = self._rpc._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) + balance_dust_level = self._config['telegram'].get('balance_dust_level', 0.0001 ) + output = '' if self._config['dry_run']: output += ( @@ -496,7 +498,7 @@ class Telegram(RPCHandler): f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" ) for curr in result['currencies']: - if curr['est_stake'] > 0.0001: + if curr['est_stake'] > balance_dust_level: curr_output = ( f"*{curr['currency']}:*\n" f"\t`Available: {curr['free']:.8f}`\n" @@ -505,7 +507,7 @@ class Telegram(RPCHandler): f"\t`Est. {curr['stake']}: " f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") else: - curr_output = f"*{curr['currency']}:* not showing <1$ amount \n" + curr_output = f"*{curr['currency']}:* not showing <{balance_dust_level} {curr['stake']} amount \n" # Handle overflowing messsage length if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: From 9cb37409fda6b0d9235ec7069489fbd062f0b873 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Feb 2021 09:56:29 +0100 Subject: [PATCH 448/563] Explicitly convert starting-balance to float --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index e7111f20c..0de0c16a0 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -277,7 +277,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), - 'total_volume': results['stake_amount'].sum(), + 'total_volume': float(results['stake_amount'].sum()), 'avg_stake_amount': results['stake_amount'].mean(), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, From a13dc3cdde3d5fba51538a9ce303824bc06df574 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Feb 2021 09:03:27 +0100 Subject: [PATCH 449/563] Use sensible defaults for balance_dust_level --- docs/configuration.md | 1 + docs/telegram-usage.md | 5 ++++- freqtrade/constants.py | 7 ++++++- freqtrade/rpc/telegram.py | 8 ++++++-- tests/rpc/test_rpc_telegram.py | 2 +- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0163e1671..99a5fea04 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -97,6 +97,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `telegram.enabled` | Enable the usage of Telegram.
**Datatype:** Boolean | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String +| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`.
**Datatype:** float | `webhook.enabled` | Enable usage of Webhook notifications
**Datatype:** Boolean | `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
**Datatype:** String | `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
**Datatype:** String diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 57f2e98bd..d4a6fb118 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -83,10 +83,13 @@ Example configuration showing the different settings: "sell": "on", "buy_cancel": "silent", "sell_cancel": "on" - } + }, + "balance_dust_level": 0.01 }, ``` +`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. + ## Create a custom keyboard (command shortcut buttons) Telegram allows us to create a custom keyboard with buttons for commands. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 51f178806..c03bff0ad 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -54,6 +54,11 @@ DECIMALS_PER_COIN = { 'ETH': 5, } +DUST_PER_COIN = { + 'BTC': 0.0001, + 'ETH': 0.01 +} + # Soure files with destination directories within user-directory USER_DATA_FILES = { @@ -230,6 +235,7 @@ CONF_SCHEMA = { 'enabled': {'type': 'boolean'}, 'token': {'type': 'string'}, 'chat_id': {'type': 'string'}, + 'balance_dust_level': {'type': 'number', 'minimum': 0.0}, 'notification_settings': { 'type': 'object', 'properties': { @@ -244,7 +250,6 @@ CONF_SCHEMA = { } }, 'required': ['enabled', 'token', 'chat_id'], - 'balance_dust_level': 0.0001 }, 'webhook': { 'type': 'object', diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ad3a00292..9d05ae142 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -17,6 +17,7 @@ from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ +from freqtrade.constants import DUST_PER_COIN from freqtrade.exceptions import OperationalException from freqtrade.misc import round_coin_value from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType @@ -487,7 +488,9 @@ class Telegram(RPCHandler): result = self._rpc._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) - balance_dust_level = self._config['telegram'].get('balance_dust_level', 0.0001 ) + balance_dust_level = self._config['telegram'].get('balance_dust_level', 0.0) + if not balance_dust_level: + balance_dust_level = DUST_PER_COIN.get(self._config['stake_currency'], 1.0) output = '' if self._config['dry_run']: @@ -507,7 +510,8 @@ class Telegram(RPCHandler): f"\t`Est. {curr['stake']}: " f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") else: - curr_output = f"*{curr['currency']}:* not showing <{balance_dust_level} {curr['stake']} amount \n" + curr_output = (f"*{curr['currency']}:* not showing <{balance_dust_level} " + f"{curr['stake']} amount \n") # Handle overflowing messsage length if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f065bb4c5..922aa2de8 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -520,7 +520,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert 'Balance:' in result assert 'Est. BTC:' in result assert 'BTC: 12.00000000' in result - assert '*XRP:* not showing <1$ amount' in result + assert '*XRP:* not showing <0.0001 BTC amount' in result def test_balance_handle_empty_response(default_conf, update, mocker) -> None: From 94cab4ed71adf24974eaa7badf2aea836fe90a61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 05:27:47 +0000 Subject: [PATCH 450/563] Bump mypy from 0.790 to 0.812 Bumps [mypy](https://github.com/python/mypy) from 0.790 to 0.812. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.790...v0.812) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index fa0ead603..6ca1a4d9c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==3.0.0 flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 -mypy==0.790 +mypy==0.812 pytest==6.2.2 pytest-asyncio==0.14.0 pytest-cov==2.11.1 From aba034ff40ed0ff06904ffb7bb50a91576c4ab73 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Feb 2021 10:56:51 +0100 Subject: [PATCH 451/563] Fix mypy problem after mypy 0.800 upgrade --- freqtrade/rpc/api_server/api_schemas.py | 8 +++++--- freqtrade/rpc/api_server/api_v1.py | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 050540cc6..2738e5368 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,5 +1,5 @@ from datetime import date, datetime -from typing import Any, Dict, List, Optional, TypeVar, Union +from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel @@ -205,7 +205,8 @@ class TradeResponse(BaseModel): trades_count: int -ForceBuyResponse = TypeVar('ForceBuyResponse', TradeSchema, StatusMsg) +class ForceBuyResponse(BaseModel): + __root__: Union[TradeSchema, StatusMsg] class LockModel(BaseModel): @@ -267,7 +268,8 @@ class PlotConfig_(BaseModel): subplots: Optional[Dict[str, Any]] -PlotConfig = TypeVar('PlotConfig', PlotConfig_, Dict) +class PlotConfig(BaseModel): + __root__: Union[PlotConfig_, Dict] class StrategyListResponse(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 3588f2196..546b93afb 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -111,9 +111,9 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): trade = rpc._rpc_forcebuy(payload.pair, payload.price) if trade: - return trade.to_json() + return {'__root__': trade.to_json()} else: - return {"status": f"Error buying pair {payload.pair}."} + return {'__root__': {"status": f"Error buying pair {payload.pair}."}} @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) @@ -183,7 +183,7 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, @router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) def plot_config(rpc: RPC = Depends(get_rpc)): - return rpc._rpc_plot_config() + return {'__root__': rpc._rpc_plot_config()} @router.get('/strategies', response_model=StrategyListResponse, tags=['strategy']) From 00747a3bc353bbb5fcc2f1acc463da6117899c9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Mar 2021 05:31:43 +0000 Subject: [PATCH 452/563] Bump mkdocs-material from 6.2.8 to 7.0.3 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.2.8 to 7.0.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.2.8...7.0.3) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 94b2fca39..73ae3ad29 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.2.8 +mkdocs-material==7.0.3 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From d0fd3c289caa49c6ab41c576b1fa98772d735e68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Mar 2021 05:32:14 +0000 Subject: [PATCH 453/563] Bump ccxt from 1.42.19 to 1.42.47 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.42.19 to 1.42.47. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.42.19...1.42.47) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d17070e34..1cd7d74df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.2 -ccxt==1.42.19 +ccxt==1.42.47 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4 From 4537a48988e4a8f6a2703ad4276d7dd564fe5545 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Mar 2021 05:32:17 +0000 Subject: [PATCH 454/563] Bump arrow from 0.17.0 to 1.0.2 Bumps [arrow](https://github.com/arrow-py/arrow) from 0.17.0 to 1.0.2. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/0.17.0...1.0.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d17070e34..1508e2abe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ cryptography==3.4.6 aiohttp==3.7.4 SQLAlchemy==1.3.23 python-telegram-bot==13.3 -arrow==0.17.0 +arrow==1.0.2 cachetools==4.2.1 requests==2.25.1 urllib3==1.26.3 From bba9b9e819aef4a199c71ed1693eba13c218ce75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 07:08:44 +0100 Subject: [PATCH 455/563] Don't use __root__ directly for api response --- freqtrade/rpc/api_server/api_v1.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 546b93afb..90e3a612f 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -111,9 +111,9 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): trade = rpc._rpc_forcebuy(payload.pair, payload.price) if trade: - return {'__root__': trade.to_json()} + return ForceBuyResponse.parse_obj(trade.to_json()) else: - return {'__root__': {"status": f"Error buying pair {payload.pair}."}} + return ForceBuyResponse.parse_obj({"status": f"Error buying pair {payload.pair}."}) @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) @@ -183,7 +183,7 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, @router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) def plot_config(rpc: RPC = Depends(get_rpc)): - return {'__root__': rpc._rpc_plot_config()} + return PlotConfig.parse_obj(rpc._rpc_plot_config()) @router.get('/strategies', response_model=StrategyListResponse, tags=['strategy']) From 3d65ba2dcbb72f8e96e6ea4e3df65cc34eed5035 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 07:51:33 +0100 Subject: [PATCH 456/563] Add rpc method to delete locks --- freqtrade/persistence/models.py | 1 + freqtrade/rpc/rpc.py | 24 ++++++++++++++++++++++-- tests/rpc/test_rpc.py | 21 ++++++++++++++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index dff59819c..3c9a10fb7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -765,6 +765,7 @@ class PairLock(_DECL_BASE): def to_json(self) -> Dict[str, Any]: return { + 'id': self.id, 'pair': self.pair, 'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT), 'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7549c38be..37a2dc1e5 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -3,7 +3,7 @@ This module contains class to define a RPC communications """ import logging from abc import abstractmethod -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from enum import Enum from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union @@ -20,6 +20,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler from freqtrade.misc import shorten_date from freqtrade.persistence import PairLocks, Trade +from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State @@ -663,7 +664,7 @@ class RPC: } def _rpc_locks(self) -> Dict[str, Any]: - """ Returns the current locks""" + """ Returns the current locks """ locks = PairLocks.get_pair_locks(None) return { @@ -671,6 +672,25 @@ class RPC: 'locks': [lock.to_json() for lock in locks] } + def _rpc_delete_lock(self, lockid: Optional[int] = None, + pair: Optional[str] = None) -> Dict[str, Any]: + """ Delete specific lock(s) """ + locks = [] + + if pair: + locks = PairLocks.get_pair_locks(pair) + if lockid: + locks = PairLock.query.filter(PairLock.id == lockid).all() + + for lock in locks: + lock.active = False + lock.lock_end_time = datetime.now(timezone.utc) + + # session is always the same + PairLock.session.flush() + + return self._rpc_locks() + def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" res = {'method': self._freqtrade.pairlists.name_list, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 60d9950aa..f745be506 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1,7 +1,8 @@ # pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments -from datetime import datetime +from datetime import datetime, timedelta, timezone +from freqtrade.persistence.pairlock_middleware import PairLocks from unittest.mock import ANY, MagicMock, PropertyMock import pytest @@ -911,6 +912,24 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None: rpc._rpc_forcebuy(pair, None) +@pytest.mark.usefixtures("init_persistence") +def test_rpc_delete_lock(mocker, default_conf): + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(freqtradebot) + pair = 'ETH/BTC' + + PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=4)) + PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=5)) + PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=10)) + locks = rpc._rpc_locks() + assert locks['lock_count'] == 3 + locks1 = rpc._rpc_delete_lock(lockid=locks['locks'][0]['id']) + assert locks1['lock_count'] == 2 + + locks2 = rpc._rpc_delete_lock(pair=pair) + assert locks2['lock_count'] == 0 + + def test_rpc_whitelist(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) From 2083cf6ddf859ade1a9fb5e4c6f89ea5d4ad5c99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 08:57:57 +0100 Subject: [PATCH 457/563] Fix mypy errors introduced by Arrow update --- freqtrade/optimize/backtesting.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3186313e1..25ec3299d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -443,16 +443,14 @@ class Backtesting: data, timerange = self.load_bt_data() - min_date = None - max_date = None for strat in self.strategylist: min_date, max_date = self.backtest_one_strategy(strat, data, timerange) + if len(self.strategylist) > 0: + stats = generate_backtest_stats(data, self.all_results, + min_date=min_date, max_date=max_date) - stats = generate_backtest_stats(data, self.all_results, - min_date=min_date, max_date=max_date) + if self.config.get('export', False): + store_backtest_stats(self.config['exportfilename'], stats) - if self.config.get('export', False): - store_backtest_stats(self.config['exportfilename'], stats) - - # Show backtest results - show_backtest_results(self.config, stats) + # Show backtest results + show_backtest_results(self.config, stats) From 64ef7becc70b5a5078298fea3a1438835286091f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 09:35:35 +0100 Subject: [PATCH 458/563] Update styles to work with new mkdocs version --- docs/index.md | 4 ---- docs/partials/header.html | 47 ++++++++++++++++++++++++++++----------- mkdocs.yml | 46 ++++++++++++++++++++------------------ 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/docs/index.md b/docs/index.md index 9d1a1532e..61f2276c3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,12 +5,8 @@ Star - Fork - Download - -Follow @freqtrade ## Introduction diff --git a/docs/partials/header.html b/docs/partials/header.html index f5243225b..22132bc96 100644 --- a/docs/partials/header.html +++ b/docs/partials/header.html @@ -6,22 +6,22 @@ This file was automatically generated - do not edit {% set site_url = site_url ~ "/index.html" %} {% endif %}
- - - - + + +
diff --git a/mkdocs.yml b/mkdocs.yml index ca52627cb..2520ca929 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,5 @@ site_name: Freqtrade +repo_url: https://github.com/freqtrade/freqtrade nav: - Home: index.md - Quickstart with Docker: docker_quickstart.md @@ -13,8 +14,8 @@ nav: - Start the bot: bot-usage.md - Control the bot: - Telegram: telegram-usage.md - - Web Hook: webhook-config.md - REST API & FreqUI: rest-api.md + - Web Hook: webhook-config.md - Data Downloading: data-download.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md @@ -50,24 +51,25 @@ extra_javascript: - https://polyfill.io/v3/polyfill.min.js?features=es6 - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js markdown_extensions: - - admonition - - footnotes - - codehilite: - guess_lang: false - - toc: - permalink: true - - pymdownx.arithmatex: - generic: true - - pymdownx.details - - pymdownx.inlinehilite - - pymdownx.magiclink - - pymdownx.pathconverter - - pymdownx.smartsymbols - - pymdownx.snippets: - base_path: docs - check_paths: true - - pymdownx.tabbed - - pymdownx.superfences - - pymdownx.tasklist: - custom_checkbox: true - - mdx_truly_sane_lists + - attr_list + - admonition + - footnotes + - codehilite: + guess_lang: false + - toc: + permalink: true + - pymdownx.arithmatex: + generic: true + - pymdownx.details + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.pathconverter + - pymdownx.smartsymbols + - pymdownx.snippets: + base_path: docs + check_paths: true + - pymdownx.tabbed + - pymdownx.superfences + - pymdownx.tasklist: + custom_checkbox: true + - mdx_truly_sane_lists From 4e5136405788d5f0a593083bfbb9a8b31187bc62 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 19:12:02 +0100 Subject: [PATCH 459/563] Add warning about sandboxes closes #4468 --- docs/sandbox-testing.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sandbox-testing.md b/docs/sandbox-testing.md index 9c14412de..5f572eba8 100644 --- a/docs/sandbox-testing.md +++ b/docs/sandbox-testing.md @@ -6,6 +6,10 @@ With some configuration, freqtrade (in combination with ccxt) provides access to This document is an overview to configure Freqtrade to be used with sandboxes. This can be useful to developers and trader alike. +!!! Warning + Sandboxes usually have very low volume, and either a very wide spread, or no orders available at all. + Therefore, sandboxes will usually not do a good job of showing you how a strategy would work in real trading. + ## Exchanges known to have a sandbox / testnet * [binance](https://testnet.binance.vision/) From 6640156ac70f36ff328c0fa2a05ed0ed1d5d8970 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 19:50:39 +0100 Subject: [PATCH 460/563] Support deleting locks via API --- docs/rest-api.md | 9 +++++++++ freqtrade/rpc/api_server/api_schemas.py | 6 ++++++ freqtrade/rpc/api_server/api_v1.py | 14 ++++++++++++-- scripts/rest_client.py | 8 ++++++++ tests/rpc/test_rpc_apiserver.py | 10 ++++++++++ 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index e2b94f080..c41c3f24c 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -131,6 +131,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `status` | Lists all open trades. | `count` | Displays number of trades used and available. | `locks` | Displays currently locked pairs. +| `delete_lock ` | Deletes (disables) the lock by id. | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). @@ -182,6 +183,11 @@ count daily Return the amount of open trades. +delete_lock + Delete (disable) lock from the database. + + :param lock_id: ID for the lock to delete + delete_trade Delete trade from the database. Tries to close open orders. Requires manual handling of this asset on the exchange. @@ -202,6 +208,9 @@ forcesell :param tradeid: Id of the trade (can be received via status command) +locks + Return current locks + logs Show latest logs. diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 2738e5368..244c5540a 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -210,6 +210,7 @@ class ForceBuyResponse(BaseModel): class LockModel(BaseModel): + id: int active: bool lock_end_time: str lock_end_timestamp: int @@ -224,6 +225,11 @@ class Locks(BaseModel): locks: List[LockModel] +class DeleteLockRequest(BaseModel): + pair: Optional[str] + lockid: Optional[int] + + class Logs(BaseModel): log_count: int logs: List[List] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 90e3a612f..7f1179a0b 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -11,7 +11,7 @@ from freqtrade.data.history import get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, - BlacklistResponse, Count, Daily, DeleteTrade, + BlacklistResponse, Count, Daily, DeleteLockRequest, DeleteTrade, ForceBuyPayload, ForceBuyResponse, ForceSellPayload, Locks, Logs, OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, @@ -136,11 +136,21 @@ def whitelist(rpc: RPC = Depends(get_rpc)): return rpc._rpc_whitelist() -@router.get('/locks', response_model=Locks, tags=['info']) +@router.get('/locks', response_model=Locks, tags=['info', 'locks']) def locks(rpc: RPC = Depends(get_rpc)): return rpc._rpc_locks() +@router.delete('/locks/{lockid}', response_model=Locks, tags=['info', 'locks']) +def delete_lock(lockid: int, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_delete_lock(lockid=lockid) + + +@router.post('/locks/delete', response_model=Locks, tags=['info', 'locks']) +def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair) + + @router.get('/logs', response_model=Logs, tags=['info']) def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)): return rpc._rpc_get_logs(limit) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index b6e66cfa4..90d2e24d4 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -118,6 +118,14 @@ class FtRestClient(): """ return self._get("locks") + def delete_lock(self, lock_id): + """Delete (disable) lock from the database. + + :param lock_id: ID for the lock to delete + :return: json object + """ + return self._delete("locks/{}".format(lock_id)) + def daily(self, days=None): """Return the amount of open trades. diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d7d69d0ae..56a496de2 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -418,6 +418,16 @@ def test_api_locks(botclient): assert 'randreason' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason']) assert 'deadbeef' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason']) + # Test deletions + rc = client_delete(client, f"{BASE_URI}/locks/1") + assert_response(rc) + assert rc.json()['lock_count'] == 1 + + rc = client_post(client, f"{BASE_URI}/locks/delete", + data='{"pair": "XRP/BTC"}') + assert_response(rc) + assert rc.json()['lock_count'] == 0 + def test_api_show_config(botclient, mocker): ftbot, client = botclient From 007ac7abb53a377af8aadd90c8be01ed71e9ce35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 20:08:49 +0100 Subject: [PATCH 461/563] Add telegram pair unlocking --- docs/telegram-usage.md | 1 + freqtrade/rpc/api_server/api_v1.py | 15 ++++++++------- freqtrade/rpc/telegram.py | 29 +++++++++++++++++++++++++++-- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_telegram.py | 13 ++++++++++++- 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index d4a6fb118..833fae1fe 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -146,6 +146,7 @@ official commands. You can ask at any moment for help with `/help`. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. +| `/unlock ` | Remove the lock for this pair (or for this lock id). | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 7f1179a0b..b983402e9 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -11,13 +11,14 @@ from freqtrade.data.history import get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, - BlacklistResponse, Count, Daily, DeleteLockRequest, DeleteTrade, - ForceBuyPayload, ForceBuyResponse, - ForceSellPayload, Locks, Logs, OpenTradeSchema, - PairHistory, PerformanceEntry, Ping, PlotConfig, - Profit, ResultMsg, ShowConfig, Stats, StatusMsg, - StrategyListResponse, StrategyResponse, - TradeResponse, Version, WhitelistResponse) + BlacklistResponse, Count, Daily, + DeleteLockRequest, DeleteTrade, ForceBuyPayload, + ForceBuyResponse, ForceSellPayload, Locks, Logs, + OpenTradeSchema, PairHistory, PerformanceEntry, + Ping, PlotConfig, Profit, ResultMsg, ShowConfig, + Stats, StatusMsg, StrategyListResponse, + StrategyResponse, TradeResponse, Version, + WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9d05ae142..fc9676a49 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -6,6 +6,7 @@ This module manage Telegram communication import json import logging from datetime import timedelta +from html import escape from itertools import chain from typing import Any, Callable, Dict, List, Union @@ -144,6 +145,7 @@ class Telegram(RPCHandler): CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler('locks', self._locks), + CommandHandler(['unlock', 'delete_locks'], self._delete_locks), CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler('stopbuy', self._stopbuy), @@ -722,17 +724,39 @@ class Telegram(RPCHandler): try: locks = self._rpc._rpc_locks() message = tabulate([[ + lock['id'], lock['pair'], lock['lock_end_time'], lock['reason']] for lock in locks['locks']], - headers=['Pair', 'Until', 'Reason'], + headers=['ID', 'Pair', 'Until', 'Reason'], tablefmt='simple') - message = "
{}
".format(message) + message = f"
{escape(message)}
" logger.debug(message) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _delete_locks(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /delete_locks. + Returns the currently active locks + """ + try: + arg = context.args[0] if context.args and len(context.args) > 0 else None + lockid = None + pair = None + if arg: + try: + lockid = int(arg) + except ValueError: + pair = arg + + self._rpc._rpc_delete_lock(lockid=lockid, pair=pair) + self._locks(update, context) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _whitelist(self, update: Update, context: CallbackContext) -> None: """ @@ -850,6 +874,7 @@ class Telegram(RPCHandler): "Avg. holding durationsfor buys and sells.`\n" "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/locks:* `Show currently locked pairs`\n" + "*/unlock :* `Unlock this Pair (or this lock id if it's numeric)`\n" "*/balance:* `Show account balance per currency`\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/reload_config:* `Reload configuration file` \n" diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index f745be506..a22accab5 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -2,7 +2,6 @@ # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments from datetime import datetime, timedelta, timezone -from freqtrade.persistence.pairlock_middleware import PairLocks from unittest.mock import ANY, MagicMock, PropertyMock import pytest @@ -11,6 +10,7 @@ from numpy import isnan from freqtrade.edge import PairInfo from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade +from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 922aa2de8..0d86c578a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -92,7 +92,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], " - "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " + "['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], " + "['show_config', 'show_conf'], ['stopbuy'], " "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" "]") @@ -981,6 +982,16 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None assert 'deadbeef' in msg_mock.call_args_list[0][0][0] assert 'randreason' in msg_mock.call_args_list[0][0][0] + context = MagicMock() + context.args = ['XRP/BTC'] + msg_mock.reset_mock() + telegram._delete_locks(update=update, context=context) + + assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] + assert 'randreason' in msg_mock.call_args_list[0][0][0] + assert 'XRP/BTC' not in msg_mock.call_args_list[0][0][0] + assert 'deadbeef' not in msg_mock.call_args_list[0][0][0] + def test_whitelist_static(default_conf, update, mocker) -> None: From 4bb6a27745df744442f10db7f0e4fd30a76d89b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Mar 2021 06:59:58 +0100 Subject: [PATCH 462/563] Don't catch errors that can't happen --- freqtrade/rpc/telegram.py | 48 +++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fc9676a49..168ae0e6a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -721,20 +721,17 @@ class Telegram(RPCHandler): Handler for /locks. Returns the currently active locks """ - try: - locks = self._rpc._rpc_locks() - message = tabulate([[ - lock['id'], - lock['pair'], - lock['lock_end_time'], - lock['reason']] for lock in locks['locks']], - headers=['ID', 'Pair', 'Until', 'Reason'], - tablefmt='simple') - message = f"
{escape(message)}
" - logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) - except RPCException as e: - self._send_msg(str(e)) + locks = self._rpc._rpc_locks() + message = tabulate([[ + lock['id'], + lock['pair'], + lock['lock_end_time'], + lock['reason']] for lock in locks['locks']], + headers=['ID', 'Pair', 'Until', 'Reason'], + tablefmt='simple') + message = f"
{escape(message)}
" + logger.debug(message) + self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only def _delete_locks(self, update: Update, context: CallbackContext) -> None: @@ -742,20 +739,17 @@ class Telegram(RPCHandler): Handler for /delete_locks. Returns the currently active locks """ - try: - arg = context.args[0] if context.args and len(context.args) > 0 else None - lockid = None - pair = None - if arg: - try: - lockid = int(arg) - except ValueError: - pair = arg + arg = context.args[0] if context.args and len(context.args) > 0 else None + lockid = None + pair = None + if arg: + try: + lockid = int(arg) + except ValueError: + pair = arg - self._rpc._rpc_delete_lock(lockid=lockid, pair=pair) - self._locks(update, context) - except RPCException as e: - self._send_msg(str(e)) + self._rpc._rpc_delete_lock(lockid=lockid, pair=pair) + self._locks(update, context) @authorized_only def _whitelist(self, update: Update, context: CallbackContext) -> None: From 7c35d107abfbba0152baa04f8c3ad2c4f3607502 Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 14:24:00 +0530 Subject: [PATCH 463/563] rest_client.py first --- scripts/rest_client.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index b6e66cfa4..eb084f400 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -75,7 +75,7 @@ class FtRestClient(): :return: json object """ return self._post("start") - + def stop(self): """Stop the bot. Use `start` to restart. @@ -174,6 +174,14 @@ class FtRestClient(): """ return self._get("show_config") + def ping(self): + """simple ping""" + + if self.show_config()['state']=="running": + return {"status": "pong"} + else: + return{"status": "not_running"} + def logs(self, limit=None): """Show latest logs. From 4fe2e542b4d9d63c17ca7f3490f25c7b823b7c7a Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 14:25:37 +0530 Subject: [PATCH 464/563] rest_client.py removing tab --- scripts/rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index eb084f400..183a906df 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -75,7 +75,7 @@ class FtRestClient(): :return: json object """ return self._post("start") - + def stop(self): """Stop the bot. Use `start` to restart. From 82bf65f696af779bfbe537e813de6f2492b76c10 Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 14:49:33 +0530 Subject: [PATCH 465/563] rest_client.py flake8 issues --- scripts/rest_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 183a906df..6aba92e7a 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -176,12 +176,11 @@ class FtRestClient(): def ping(self): """simple ping""" - if self.show_config()['state']=="running": return {"status": "pong"} else: - return{"status": "not_running"} - + return {"status": "not_running"} + def logs(self, limit=None): """Show latest logs. From 95c635091ef1e7f16ec5fce669d67b26af4a7abc Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 14:57:05 +0530 Subject: [PATCH 466/563] rest_client.py fixed operator --- scripts/rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 6aba92e7a..39da0a406 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -176,7 +176,7 @@ class FtRestClient(): def ping(self): """simple ping""" - if self.show_config()['state']=="running": + if self.show_config()['state'] == "running": return {"status": "pong"} else: return {"status": "not_running"} From 218d22ed528da1bcc22b6971c4ff4f4ec847eae6 Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 15:45:16 +0530 Subject: [PATCH 467/563] rest_client.py updated for connection error case --- scripts/rest_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 39da0a406..a7d7705fe 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -176,7 +176,9 @@ class FtRestClient(): def ping(self): """simple ping""" - if self.show_config()['state'] == "running": + if not self.show_config(): + return {"status": "not_running"} + elif self.show_config()['state'] == "running": return {"status": "pong"} else: return {"status": "not_running"} From a85e656e8d5320db721216365205c16160756d5f Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 16:16:20 +0530 Subject: [PATCH 468/563] rest_client.py optimised with var 'configstatus' --- scripts/rest_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index a7d7705fe..ecf961ddd 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -176,9 +176,10 @@ class FtRestClient(): def ping(self): """simple ping""" - if not self.show_config(): + configstatus = self.show_config() + if not configstatus: return {"status": "not_running"} - elif self.show_config()['state'] == "running": + elif configstatus['state'] == "running": return {"status": "pong"} else: return {"status": "not_running"} From 55a315be14c00072983bd15e5e6c1ec21ce430c3 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Tue, 2 Mar 2021 13:34:14 +0100 Subject: [PATCH 469/563] fix: avg_stake_amount should not be `NaN` if df is empty --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0de0c16a0..47ddfc9fc 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -278,7 +278,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'total_volume': float(results['stake_amount'].sum()), - 'avg_stake_amount': results['stake_amount'].mean(), + 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), From 078b77d41be278208b9bc4fb5ddfe224aa562e68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Mar 2021 16:12:22 +0100 Subject: [PATCH 470/563] Fix crash when using unlimited stake and no trades are made --- freqtrade/optimize/optimize_reports.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 47ddfc9fc..52ae09ad1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -8,7 +8,7 @@ from numpy import int64 from pandas import DataFrame from tabulate import tabulate -from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN +from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change, calculate_max_drawdown) from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value @@ -499,8 +499,10 @@ def text_table_add_metrics(strat_results: Dict) -> str: else: start_balance = round_coin_value(strat_results['starting_balance'], strat_results['stake_currency']) - stake_amount = round_coin_value(strat_results['stake_amount'], - strat_results['stake_currency']) + stake_amount = round_coin_value( + strat_results['stake_amount'], strat_results['stake_currency'] + ) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited' + message = ("No trades made. " f"Your starting balance was {start_balance}, " f"and your stake was {stake_amount}." From 0968ecc1af8c965e407c45ef038a36a5c888d1e4 Mon Sep 17 00:00:00 2001 From: raoulus Date: Thu, 4 Mar 2021 17:27:04 +0100 Subject: [PATCH 471/563] added "Median profit" column to hyperopt -> export-csv --- freqtrade/optimize/hyperopt.py | 4 ++-- tests/commands/test_commands.py | 2 +- tests/conftest.py | 24 ++++++++++++------------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index eee0f13b3..c46d0da48 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -419,13 +419,13 @@ class Hyperopt: trials['Stake currency'] = config['stake_currency'] base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.total_profit', + 'results_metrics.avg_profit', 'results_metrics.median_profit', 'results_metrics.total_profit', 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', 'loss', 'is_initial_point', 'is_best'] param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] trials = trials[base_metrics + param_metrics] - base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency', + base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', 'Stake currency', 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] param_columns = list(results[0]['params_dict'].keys()) trials.columns = base_columns + param_columns diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index c81909025..d5e76eeb6 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1145,7 +1145,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): captured = capsys.readouterr() log_has("CSV file created: test_file.csv", caplog) f = Path("test_file.csv") - assert 'Best,1,2,-1.25%,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() + assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() assert f.is_file() f.unlink() diff --git a/tests/conftest.py b/tests/conftest.py index 61899dd53..9834aa36c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1766,7 +1766,7 @@ def hyperopt_results(): 'params_dict': { 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 - 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501 + 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'median_profit': -1.2222, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 'total_profit': -0.00125625, 'current_epoch': 1, @@ -1781,7 +1781,7 @@ def hyperopt_results(): 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 'stoploss': {'stoploss': -0.338070047333259}}, - 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501 + 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'median_profit': -1.2222, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501 'total_profit': 6.185e-05, 'current_epoch': 2, @@ -1791,7 +1791,7 @@ def hyperopt_results(): 'loss': 14.241196856510731, 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 - 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501 + 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'median_profit': -1.2222, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 'total_profit': -0.13639474, 'current_epoch': 3, @@ -1801,14 +1801,14 @@ def hyperopt_results(): 'loss': 100000, 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 - 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 + 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'median_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False }, { 'loss': 0.22195522184191518, 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 - 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501 + 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'median_profit': -1.2222, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 'total_profit': -0.002480140000000001, 'current_epoch': 5, @@ -1818,7 +1818,7 @@ def hyperopt_results(): 'loss': 0.545315889154162, 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 - 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501 + 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'median_profit': -1.2222, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 'total_profit': -0.0041773, 'current_epoch': 6, @@ -1830,7 +1830,7 @@ def hyperopt_results(): 'params_details': { 'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 - 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501 + 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'median_profit': -1.2222, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 'total_profit': -0.06339929, 'current_epoch': 7, @@ -1840,7 +1840,7 @@ def hyperopt_results(): 'loss': 20.0, # noqa: E501 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 - 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501 + 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'median_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 'total_profit': 0.0, 'current_epoch': 8, @@ -1850,7 +1850,7 @@ def hyperopt_results(): 'loss': 2.4731817780991223, 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 - 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501 + 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'median_profit': -1.2222, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 'total_profit': -0.044050070000000004, # noqa: E501 'current_epoch': 9, @@ -1860,7 +1860,7 @@ def hyperopt_results(): 'loss': -0.2604606005845212, # noqa: E501 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 - 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501 + 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'median_profit': -1.2222, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 'total_profit': 0.00021629, 'current_epoch': 10, @@ -1870,7 +1870,7 @@ def hyperopt_results(): 'loss': 4.876465945994304, # noqa: E501 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 - 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501 + 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'median_profit': -1.2222, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 'total_profit': -0.07436117, 'current_epoch': 11, @@ -1880,7 +1880,7 @@ def hyperopt_results(): 'loss': 100000, 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 - 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 + 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'median_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 'total_profit': 0, 'current_epoch': 12, From d5993db064bcbd5fd1c83163e68d3e26a6af31c2 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 14:59:55 +0100 Subject: [PATCH 472/563] refactor(docs/strategy-customization): change variable name for better readability `cust_info` -> `custom_info` --- docs/strategy-customization.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index fdc95a3c1..6eaafa15c 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -311,18 +311,18 @@ The name of the variable can be chosen at will, but should be prefixed with `cus ```python class AwesomeStrategy(IStrategy): # Create custom dictionary - cust_info = {} + custom_info = {} def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Check if the entry already exists - if not metadata["pair"] in self.cust_info: + if not metadata["pair"] in self.custom_info: # Create empty entry for this pair - self.cust_info[metadata["pair"]] = {} + self.custom_info[metadata["pair"]] = {} - if "crosstime" in self.cust_info[metadata["pair"]]: - self.cust_info[metadata["pair"]]["crosstime"] += 1 + if "crosstime" in self.custom_info[metadata["pair"]]: + self.custom_info[metadata["pair"]]["crosstime"] += 1 else: - self.cust_info[metadata["pair"]]["crosstime"] = 1 + self.custom_info[metadata["pair"]]["crosstime"] = 1 ``` !!! Warning From 5cf3194fab8b026d204f8992aa0ad76c18c8bcb5 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 15:03:44 +0100 Subject: [PATCH 473/563] chore(docs/strategy-customization): clean up left over trailing whitespaces --- docs/strategy-advanced.md | 2 +- docs/strategy-customization.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index dcd340fd1..2fe29d431 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -142,7 +142,7 @@ class AwesomeStrategy(IStrategy): return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss # After reaching the desired offset, allow the stoploss to trail by half the profit - desired_stoploss = current_profit / 2 + desired_stoploss = current_profit / 2 # Use a minimum of 2.5% and a maximum of 5% return max(min(desired_stoploss, 0.05), 0.025) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6eaafa15c..983a5f60a 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -399,7 +399,7 @@ if self.dp: ### *current_whitelist()* -Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume. +Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume. The strategy might look something like this: @@ -418,7 +418,7 @@ This is where calling `self.dp.current_whitelist()` comes in handy. pairs = self.dp.current_whitelist() # Assign tf to each pair so they can be downloaded and cached for strategy. informative_pairs = [(pair, '1d') for pair in pairs] - return informative_pairs + return informative_pairs ``` ### *get_pair_dataframe(pair, timeframe)* @@ -583,7 +583,7 @@ All columns of the informative dataframe will be available on the returning data ``` python 'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe - 'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe + 'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe ``` ??? Example "Custom implementation" From cc4e84bb7009330031dfd0aadc8b2db9a1d13b49 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 15:04:18 +0100 Subject: [PATCH 474/563] feature(docs/strategy-customization): add example how to store indicator with DatetimeIndex into custom_info --- docs/strategy-customization.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 983a5f60a..0b09e073f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -333,6 +333,22 @@ class AwesomeStrategy(IStrategy): *** +#### Storing custom information using DatetimeIndex from `dataframe` + +Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. + +class AwesomeStrategy(IStrategy): + # Create custom dictionary + custom_info = {} + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # add indicator mapped to correct DatetimeIndex to custom_info + # using "ATR" here as example + self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') + return dataframe + +See: (custom_stoploss example)[WIP] for how to access it + ## Additional data (informative_pairs) ### Get data for non-tradeable pairs From c5900bbd384e3d3c22a5bc717dfddb47dc13c6a4 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 15:16:27 +0100 Subject: [PATCH 475/563] feature(docs/strategy-customization): add example "Custom stoploss using an indicator from dataframe" --- docs/strategy-advanced.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 2fe29d431..0cd4d1be0 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -179,6 +179,35 @@ class AwesomeStrategy(IStrategy): return (-0.07 + current_profit) return 1 ``` +#### Custom stoploss using an indicator from dataframe example + +Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" + +See: (Storing custom information using DatetimeIndex from `dataframe` +)[WIP] on how to store the indicator into `custom_info` + +``` python +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + result = 1 + if self.custom_info[pair] is not None and trade is not None: + atr = self.custom_info[pair].loc[current_time]['atr'] + if (atr is not None): + # new stoploss relative to current_rate + new_stoploss = (current_rate-atr)/current_rate + # turn into relative negative offset required by `custom_stoploss` return implementation + result = new_stoploss - 1 + return result +``` --- From 32f35fcd904f44b8a96e23d726da1e5f1e5484b3 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 21:26:21 +0100 Subject: [PATCH 476/563] fix(docs/strategy-customization): "custom_stoploss indicator" example need to check for RUN_MODE --- docs/strategy-advanced.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 0cd4d1be0..8c730b3df 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -188,6 +188,7 @@ See: (Storing custom information using DatetimeIndex from `dataframe` ``` python from freqtrade.persistence import Trade +from freqtrade.state import RunMode class AwesomeStrategy(IStrategy): @@ -200,12 +201,23 @@ class AwesomeStrategy(IStrategy): result = 1 if self.custom_info[pair] is not None and trade is not None: - atr = self.custom_info[pair].loc[current_time]['atr'] - if (atr is not None): + # using current_time directly (like below) will only work in backtesting. + # so check "runmode" to make sure that it's only used in backtesting + if(self.dp.runmode == RunMode.BACKTEST): + relative_sl = self.custom_info[pair].loc[current_time]['atr] + # in live / dry-run, it'll be really the current time + else: + # but we can just use the last entry to get the current value + relative_sl = self.custom_info[pair]['atr].iloc[ -1 ] + + if (relative_sl is not None): + print("Custom SL: {}".format(relative_sl)) # new stoploss relative to current_rate - new_stoploss = (current_rate-atr)/current_rate + new_stoploss = (current_rate-relative_sl)/current_rate # turn into relative negative offset required by `custom_stoploss` return implementation result = new_stoploss - 1 + + print("Result: {}".format(result)) return result ``` From d05acc30fa2819fe9ca03c224b62d3b46cae86fe Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 22:10:08 +0100 Subject: [PATCH 477/563] fix(docs/strategy-customization): remove superflous `prints` from example code --- docs/strategy-advanced.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 8c730b3df..504b7270e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -211,13 +211,11 @@ class AwesomeStrategy(IStrategy): relative_sl = self.custom_info[pair]['atr].iloc[ -1 ] if (relative_sl is not None): - print("Custom SL: {}".format(relative_sl)) # new stoploss relative to current_rate new_stoploss = (current_rate-relative_sl)/current_rate # turn into relative negative offset required by `custom_stoploss` return implementation result = new_stoploss - 1 - print("Result: {}".format(result)) return result ``` From b52698197b469d7739076b2c538f7c216f27cc8e Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 14:44:51 +0100 Subject: [PATCH 478/563] refactor(docs/strategy-advanced): extract "Storing information" section from `strategy-customization.md` --- docs/strategy-advanced.md | 47 ++++++++++++++++++++++++++++++++++ docs/strategy-customization.md | 47 ---------------------------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 504b7270e..2cd411078 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -11,6 +11,53 @@ If you're just getting started, please be familiar with the methods described in !!! Tip You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` +## Storing information + +Storing information can be accomplished by creating a new dictionary within the strategy class. + +The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. + +```python +class AwesomeStrategy(IStrategy): + # Create custom dictionary + custom_info = {} + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Check if the entry already exists + if not metadata["pair"] in self.custom_info: + # Create empty entry for this pair + self.custom_info[metadata["pair"]] = {} + + if "crosstime" in self.custom_info[metadata["pair"]]: + self.custom_info[metadata["pair"]]["crosstime"] += 1 + else: + self.custom_info[metadata["pair"]]["crosstime"] = 1 +``` + +!!! Warning + The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + +!!! Note + If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. + +*** + +### Storing custom information using DatetimeIndex from `dataframe` + +Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. + +class AwesomeStrategy(IStrategy): + # Create custom dictionary + custom_info = {} + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # add indicator mapped to correct DatetimeIndex to custom_info + # using "ATR" here as example + self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') + return dataframe + +See `custom_stoploss` examples below on how to access the saved dataframe columns + ## Custom stoploss A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 0b09e073f..a66be013e 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -302,53 +302,6 @@ Currently this is `pair`, which can be accessed using `metadata['pair']` - and w The Metadata-dict should not be modified and does not persist information across multiple calls. Instead, have a look at the section [Storing information](#Storing-information) -### Storing information - -Storing information can be accomplished by creating a new dictionary within the strategy class. - -The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. - -```python -class AwesomeStrategy(IStrategy): - # Create custom dictionary - custom_info = {} - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # Check if the entry already exists - if not metadata["pair"] in self.custom_info: - # Create empty entry for this pair - self.custom_info[metadata["pair"]] = {} - - if "crosstime" in self.custom_info[metadata["pair"]]: - self.custom_info[metadata["pair"]]["crosstime"] += 1 - else: - self.custom_info[metadata["pair"]]["crosstime"] = 1 -``` - -!!! Warning - The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. - -!!! Note - If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. - -*** - -#### Storing custom information using DatetimeIndex from `dataframe` - -Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. - -class AwesomeStrategy(IStrategy): - # Create custom dictionary - custom_info = {} - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # add indicator mapped to correct DatetimeIndex to custom_info - # using "ATR" here as example - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') - return dataframe - -See: (custom_stoploss example)[WIP] for how to access it - ## Additional data (informative_pairs) ### Get data for non-tradeable pairs From 4064f856d1b47d68dbd1d40aa09cbb47c342720c Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 14:50:04 +0100 Subject: [PATCH 479/563] fix(docs/strategy-customization): add "hyperopt" to runmode check for custom_info in custom_stoploss example --- docs/strategy-advanced.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 2cd411078..f230a2371 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -249,8 +249,8 @@ class AwesomeStrategy(IStrategy): result = 1 if self.custom_info[pair] is not None and trade is not None: # using current_time directly (like below) will only work in backtesting. - # so check "runmode" to make sure that it's only used in backtesting - if(self.dp.runmode == RunMode.BACKTEST): + # so check "runmode" to make sure that it's only used in backtesting/hyperopt + if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'): relative_sl = self.custom_info[pair].loc[current_time]['atr] # in live / dry-run, it'll be really the current time else: From 1a02a146a1e1023fba4f040e88eec3d9b8ace32a Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 14:59:08 +0100 Subject: [PATCH 480/563] feature(docs/strategy-advanced/custom_info-storage/example): add ATR column calculation --- docs/strategy-advanced.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f230a2371..81ba24a67 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -46,15 +46,19 @@ class AwesomeStrategy(IStrategy): Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. +```python +import talib.abstract as ta class AwesomeStrategy(IStrategy): # Create custom dictionary custom_info = {} def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # add indicator mapped to correct DatetimeIndex to custom_info # using "ATR" here as example + dataframe['atr'] = ta.ATR(dataframe) + # add indicator mapped to correct DatetimeIndex to custom_info self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') return dataframe +``` See `custom_stoploss` examples below on how to access the saved dataframe columns From 22a558e33120a9e16a29c53b4f00ee268e30d7fe Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 15:01:21 +0100 Subject: [PATCH 481/563] fix(docs/strategy-advanced): fix link to custom_info storage --- docs/strategy-advanced.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 81ba24a67..d685662eb 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -234,8 +234,7 @@ class AwesomeStrategy(IStrategy): Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" -See: (Storing custom information using DatetimeIndex from `dataframe` -)[WIP] on how to store the indicator into `custom_info` +See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info` ``` python from freqtrade.persistence import Trade From a6ef354a5fc5c289aee14ace1881055471370a10 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 18:52:23 +0100 Subject: [PATCH 482/563] fix(docs/strategy-advanced): use `get_analyzed_dataframe()` instead of `custom_info.iloc` --- docs/strategy-advanced.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index d685662eb..42ffaa423 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -257,8 +257,11 @@ class AwesomeStrategy(IStrategy): relative_sl = self.custom_info[pair].loc[current_time]['atr] # in live / dry-run, it'll be really the current time else: + # but we can just use the last entry from an already analyzed dataframe instead + dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, + timeframe=self.timeframe) # but we can just use the last entry to get the current value - relative_sl = self.custom_info[pair]['atr].iloc[ -1 ] + relative_sl = dataframe['atr'].iat[-1] if (relative_sl is not None): # new stoploss relative to current_rate From c56b9cd75172ef43d3de33814fd79c3c1f1a3ec1 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 18:50:48 +0100 Subject: [PATCH 483/563] fix(docs/strategy-advanced): add warnings --- docs/strategy-advanced.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 42ffaa423..4cc1adb49 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -60,6 +60,12 @@ class AwesomeStrategy(IStrategy): return dataframe ``` +!!! Warning + The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + +!!! Note + If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. + See `custom_stoploss` examples below on how to access the saved dataframe columns ## Custom stoploss @@ -236,6 +242,11 @@ Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info` +!!! Warning + only use .iat[-1] in live mode, not in backtesting/hyperopt + otherwise you will look into the future + see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies + ``` python from freqtrade.persistence import Trade from freqtrade.state import RunMode @@ -260,7 +271,10 @@ class AwesomeStrategy(IStrategy): # but we can just use the last entry from an already analyzed dataframe instead dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) - # but we can just use the last entry to get the current value + # WARNING + # only use .iat[-1] in live mode, not in backtesting/hyperopt + # otherwise you will look into the future + # see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies relative_sl = dataframe['atr'].iat[-1] if (relative_sl is not None): From 900deb663a282b5e234993e6b9b36ccc5a4df487 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 19:58:43 +0100 Subject: [PATCH 484/563] fix(docs/strategy-advanced/custom_stoploss/example): check if "pair" exists in "custom_info" before requesting --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4cc1adb49..c166f87fb 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -261,7 +261,7 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: result = 1 - if self.custom_info[pair] is not None and trade is not None: + if self.custom_info and pair in self.custom_info and trade: # using current_time directly (like below) will only work in backtesting. # so check "runmode" to make sure that it's only used in backtesting/hyperopt if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'): From 1304918a29de43e34b207b21a6f9b3b92bd79023 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 19:59:57 +0100 Subject: [PATCH 485/563] fix(docs/strategy-advanced/custom_info-storage/example): only add to "custom_info" in backtesting and hyperopt --- docs/strategy-advanced.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index c166f87fb..5c7ae83bc 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -55,8 +55,9 @@ class AwesomeStrategy(IStrategy): def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # using "ATR" here as example dataframe['atr'] = ta.ATR(dataframe) - # add indicator mapped to correct DatetimeIndex to custom_info - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') + if self.dp.runmode.value in ('backtest', 'hyperopt'): + # add indicator mapped to correct DatetimeIndex to custom_info + self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') return dataframe ``` From 161a4656d5769ac3942fc2f1ec96d971bf224494 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 4 Mar 2021 20:05:21 +0100 Subject: [PATCH 486/563] Update docs/strategy-advanced.md Co-authored-by: Matthias --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 5c7ae83bc..56061365e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -246,7 +246,7 @@ See: "Storing custom information using DatetimeIndex from `dataframe`" example a !!! Warning only use .iat[-1] in live mode, not in backtesting/hyperopt otherwise you will look into the future - see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies + see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. ``` python from freqtrade.persistence import Trade From dfeafc22044b169c99c39436ac9679ea31211afc Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 4 Mar 2021 20:05:27 +0100 Subject: [PATCH 487/563] Update docs/strategy-customization.md Co-authored-by: Matthias --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a66be013e..aebc51509 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -300,7 +300,7 @@ The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `p Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`. The Metadata-dict should not be modified and does not persist information across multiple calls. -Instead, have a look at the section [Storing information](#Storing-information) +Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information) ## Additional data (informative_pairs) From bc05d03126aa7a9622b2fa75552c1e026c17d0f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Mar 2021 19:21:09 +0100 Subject: [PATCH 488/563] Make best / worst day absolute --- docs/backtesting.md | 10 +++++----- freqtrade/optimize/optimize_reports.py | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 2e91b6e74..d02c59f05 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -289,8 +289,8 @@ A backtesting result will look like that: | Worst Pair | ZEC/BTC -10.18% | | Best Trade | LSK/BTC 4.25% | | Worst Trade | ZEC/BTC -10.25% | -| Best day | 25.27% | -| Worst day | -30.67% | +| Best day | 0.00076 BTC | +| Worst day | -0.00036 BTC | | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | @@ -376,8 +376,8 @@ It contains some useful key metrics about performance of your strategy on backte | Worst Pair | ZEC/BTC -10.18% | | Best Trade | LSK/BTC 4.25% | | Worst Trade | ZEC/BTC -10.25% | -| Best day | 25.27% | -| Worst day | -30.67% | +| Best day | 0.00076 BTC | +| Worst day | -0.00036 BTC | | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | @@ -406,7 +406,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. -- `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade +- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade. - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 52ae09ad1..099976aa9 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -196,13 +196,18 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: return { 'backtest_best_day': 0, 'backtest_worst_day': 0, + 'backtest_best_day_abs': 0, + 'backtest_worst_day_abs': 0, 'winning_days': 0, 'draw_days': 0, 'losing_days': 0, 'winner_holding_avg': timedelta(), 'loser_holding_avg': timedelta(), } - daily_profit = results.resample('1d', on='close_date')['profit_ratio'].sum() + daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum() + daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10) + worst_rel = min(daily_profit_rel) + best_rel = max(daily_profit_rel) worst = min(daily_profit) best = max(daily_profit) winning_days = sum(daily_profit > 0) @@ -213,8 +218,10 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: losing_trades = results.loc[results['profit_ratio'] < 0] return { - 'backtest_best_day': best, - 'backtest_worst_day': worst, + 'backtest_best_day': best_rel, + 'backtest_worst_day': worst_rel, + 'backtest_best_day_abs': best, + 'backtest_worst_day_abs': worst, 'winning_days': winning_days, 'draw_days': draw_days, 'losing_days': losing_days, @@ -470,8 +477,10 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Worst trade', f"{worst_trade['pair']} " f"{round(worst_trade['profit_ratio'] * 100, 2)}%"), - ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), - ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), + ('Best day', round_coin_value(strat_results['backtest_best_day_abs'], + strat_results['stake_currency'])), + ('Worst day', round_coin_value(strat_results['backtest_worst_day_abs'], + strat_results['stake_currency'])), ('Days win/draw/lose', f"{strat_results['winning_days']} / " f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), From 731ab5d2a775ff5f10d345e24066ac0398cf597b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Mar 2021 19:22:57 +0100 Subject: [PATCH 489/563] Fix too long line errors --- freqtrade/optimize/hyperopt.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index c46d0da48..955f97f33 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -419,14 +419,16 @@ class Hyperopt: trials['Stake currency'] = config['stake_currency'] base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.median_profit', 'results_metrics.total_profit', + 'results_metrics.avg_profit', 'results_metrics.median_profit', + 'results_metrics.total_profit', 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', 'loss', 'is_initial_point', 'is_best'] param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] trials = trials[base_metrics + param_metrics] - base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', 'Stake currency', - 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] + base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', + 'Stake currency', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] param_columns = list(results[0]['params_dict'].keys()) trials.columns = base_columns + param_columns From 345f7404e990451e4e2024b20df9d9f0a975cdf0 Mon Sep 17 00:00:00 2001 From: Patrick Weber Date: Fri, 5 Mar 2021 12:56:11 -0600 Subject: [PATCH 490/563] Add strategy name to HyperOpt results filename This just extends the HyperOpt result filename by adding the strategy name. This allows analysis of HyperOpt results folder with no additional necessary context. An alternative idea would be to expand the result dict, but the additional static copies are non value added. --- freqtrade/optimize/hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 955f97f33..66e11cf68 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -77,8 +77,9 @@ class Hyperopt: self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + strategy = str(self.config['strategy']) self.results_file = (self.config['user_data_dir'] / - 'hyperopt_results' / f'hyperopt_results_{time_now}.pickle') + 'hyperopt_results' / f'strategy_{strategy}_' f'hyperopt_results_{time_now}.pickle') self.data_pickle_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) From 5196306407a7d0c764d961d43e19c07ccefc4161 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Mar 2021 20:03:49 +0100 Subject: [PATCH 491/563] Remove deprecated profit return value --- freqtrade/rpc/api_server/api_schemas.py | 2 -- freqtrade/rpc/rpc.py | 2 -- tests/rpc/test_rpc.py | 8 ++++---- tests/rpc/test_rpc_apiserver.py | 2 -- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 244c5540a..32a1c8597 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -62,14 +62,12 @@ class PerformanceEntry(BaseModel): class Profit(BaseModel): profit_closed_coin: float - profit_closed_percent: float profit_closed_percent_mean: float profit_closed_ratio_mean: float profit_closed_percent_sum: float profit_closed_ratio_sum: float profit_closed_fiat: float profit_all_coin: float - profit_all_percent: float profit_all_percent_mean: float profit_all_ratio_mean: float profit_all_percent_sum: float diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 37a2dc1e5..fa830486e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -402,14 +402,12 @@ class RPC: num = float(len(durations) or 1) return { 'profit_closed_coin': profit_closed_coin_sum, - 'profit_closed_percent': round(profit_closed_ratio_mean * 100, 2), # DEPRECATED 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), 'profit_closed_ratio_mean': profit_closed_ratio_mean, 'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2), 'profit_closed_ratio_sum': profit_closed_ratio_sum, 'profit_closed_fiat': profit_closed_fiat, 'profit_all_coin': profit_all_coin_sum, - 'profit_all_percent': round(profit_all_ratio_mean * 100, 2), # DEPRECATED 'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2), 'profit_all_ratio_mean': profit_all_ratio_mean, 'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2), diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a22accab5..b11470711 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -413,10 +413,10 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05) - assert prec_satoshi(stats['profit_closed_percent'], 6.2) + assert prec_satoshi(stats['profit_closed_percent_mean'], 6.2) assert prec_satoshi(stats['profit_closed_fiat'], 0.93255) assert prec_satoshi(stats['profit_all_coin'], 5.802e-05) - assert prec_satoshi(stats['profit_all_percent'], 2.89) + assert prec_satoshi(stats['profit_all_percent_mean'], 2.89) assert prec_satoshi(stats['profit_all_fiat'], 0.8703) assert stats['trade_count'] == 2 assert stats['first_trade_date'] == 'just now' @@ -482,10 +482,10 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert prec_satoshi(stats['profit_closed_coin'], 0) - assert prec_satoshi(stats['profit_closed_percent'], 0) + assert prec_satoshi(stats['profit_closed_percent_mean'], 0) assert prec_satoshi(stats['profit_closed_fiat'], 0) assert prec_satoshi(stats['profit_all_coin'], 0) - assert prec_satoshi(stats['profit_all_percent'], 0) + assert prec_satoshi(stats['profit_all_percent_mean'], 0) assert prec_satoshi(stats['profit_all_fiat'], 0) assert stats['trade_count'] == 1 assert stats['first_trade_date'] == 'just now' diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 56a496de2..8590e0d21 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -624,14 +624,12 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li 'latest_trade_timestamp': ANY, 'profit_all_coin': 6.217e-05, 'profit_all_fiat': 0.76748865, - 'profit_all_percent': 6.2, 'profit_all_percent_mean': 6.2, 'profit_all_ratio_mean': 0.06201058, 'profit_all_percent_sum': 6.2, 'profit_all_ratio_sum': 0.06201058, 'profit_closed_coin': 6.217e-05, 'profit_closed_fiat': 0.76748865, - 'profit_closed_percent': 6.2, 'profit_closed_ratio_mean': 0.06201058, 'profit_closed_percent_mean': 6.2, 'profit_closed_ratio_sum': 0.06201058, From 45322220107ea447b8a0d7626cc6bb1bdd388bdb Mon Sep 17 00:00:00 2001 From: Patrick Weber Date: Fri, 5 Mar 2021 13:16:49 -0600 Subject: [PATCH 492/563] Fixed line length in HyperOpt for new name Fixed line length errors and multiple f strings to facilitate strategy being added in the name --- freqtrade/optimize/hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 66e11cf68..9001a3657 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -79,7 +79,8 @@ class Hyperopt: time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") strategy = str(self.config['strategy']) self.results_file = (self.config['user_data_dir'] / - 'hyperopt_results' / f'strategy_{strategy}_' f'hyperopt_results_{time_now}.pickle') + 'hyperopt_results' / + f'strategy_{strategy}_hyperopt_results_{time_now}.pickle') self.data_pickle_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) From a405d578da0a6eaa6e0e1e5f794672771b094ad3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Mar 2021 20:22:04 +0100 Subject: [PATCH 493/563] Introduce forcebuy ordertype to allow specifiying a different ordertype for forcebuy / forcesells --- config_full.json.example | 1 + docs/configuration.md | 6 ++++-- docs/stoploss.md | 4 ++++ freqtrade/constants.py | 2 ++ freqtrade/freqtradebot.py | 7 ++++++- freqtrade/rpc/rpc.py | 2 +- 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 9a613c0a1..8366774c4 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -50,6 +50,7 @@ "sell": "limit", "emergencysell": "market", "forcesell": "market", + "forcebuy": "market", "stoploss": "market", "stoploss_on_exchange": false, "stoploss_on_exchange_interval": 60 diff --git a/docs/configuration.md b/docs/configuration.md index 99a5fea04..83ffbbff9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -278,7 +278,7 @@ For example, if your strategy is using a 1h timeframe, and you only want to buy ### Understand order_types -The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. +The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`, `forcebuy`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using limit-orders, and create stoplosses using market orders. It also allows to set the @@ -290,7 +290,7 @@ the buy order is fulfilled. If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and `stoploss_on_exchange`) need to be present, otherwise the bot will fail to start. -For information on (`emergencysell`,`forcesell`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md) +For information on (`emergencysell`,`forcesell`, `forcebuy`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md) Syntax for Strategy: @@ -299,6 +299,7 @@ order_types = { "buy": "limit", "sell": "limit", "emergencysell": "market", + "forcebuy": "market", "forcesell": "market", "stoploss": "market", "stoploss_on_exchange": False, @@ -314,6 +315,7 @@ Configuration: "buy": "limit", "sell": "limit", "emergencysell": "market", + "forcebuy": "market", "forcesell": "market", "stoploss": "market", "stoploss_on_exchange": false, diff --git a/docs/stoploss.md b/docs/stoploss.md index 4a4391655..ae191f639 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -55,6 +55,10 @@ This same logic will reapply a stoploss order on the exchange should you cancel `forcesell` is an optional value, which defaults to the same value as `sell` and is used when sending a `/forcesell` command from Telegram or from the Rest API. +### forcebuy + +`forcebuy` is an optional value, which defaults to the same value as `buy` and is used when sending a `/forcebuy` command from Telegram or from the Rest API. + ### emergencysell `emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c03bff0ad..06eaad4f9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -179,6 +179,8 @@ CONF_SCHEMA = { 'properties': { 'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, + 'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, + 'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss_on_exchange': {'type': 'boolean'}, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2f64f3dac..f605d61c4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -520,7 +520,8 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") return False - def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool: + def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None, + forcebuy: bool = False) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -548,6 +549,10 @@ class FreqtradeBot(LoggingMixin): amount = stake_amount / buy_limit_requested order_type = self.strategy.order_types['buy'] + if forcebuy: + # Forcebuy can define a different ordertype + order_type = self.strategy.order_types.get('forcebuy', order_type) + if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index fa830486e..61e22234d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -593,7 +593,7 @@ class RPC: pair, self._freqtrade.get_free_open_trades()) # execute buy - if self._freqtrade.execute_buy(pair, stakeamount, price): + if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True): trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade else: From 03b89e7f7829ebeb24f79df45f021360af580ce7 Mon Sep 17 00:00:00 2001 From: Th0masL Date: Sat, 6 Mar 2021 00:04:12 +0200 Subject: [PATCH 494/563] Add trade_id in Telegram messages --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 168ae0e6a..fb93caee2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -193,7 +193,7 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 - message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}\n" + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']} (#{msg['trade_id']})\n" f"*Amount:* `{msg['amount']:.8f}`\n" f"*Open Rate:* `{msg['limit']:.8f}`\n" f"*Current Rate:* `{msg['current_rate']:.8f}`\n" @@ -216,7 +216,7 @@ class Telegram(RPCHandler): msg['emoji'] = self._get_sell_emoji(msg) - message = ("{emoji} *{exchange}:* Selling {pair}\n" + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" From 2472f52874661c79566adbd08469ddd131df9388 Mon Sep 17 00:00:00 2001 From: Th0masL Date: Sat, 6 Mar 2021 01:07:37 +0200 Subject: [PATCH 495/563] Add trade_id to tests --- tests/rpc/test_rpc_telegram.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0d86c578a..86b978f3b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1196,6 +1196,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { 'type': RPCMessageType.BUY_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'limit': 1.099e-05, @@ -1212,7 +1213,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ - == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ + == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1256,6 +1257,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', 'gain': 'loss', @@ -1273,7 +1275,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' + == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1285,6 +1287,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: msg_mock.reset_mock() telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', 'gain': 'loss', @@ -1301,7 +1304,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' + == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1384,6 +1387,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'limit': 1.099e-05, @@ -1396,7 +1400,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' + assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' @@ -1409,6 +1413,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', 'gain': 'loss', @@ -1425,7 +1430,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), 'close_date': arrow.utcnow(), }) - assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' + assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' From ad0e60b5b662b20be9b2aa4cf799e4e387809aa9 Mon Sep 17 00:00:00 2001 From: Th0masL Date: Sat, 6 Mar 2021 15:07:47 +0200 Subject: [PATCH 496/563] Add trade_id to Cancel messages and reduced lines length --- freqtrade/rpc/telegram.py | 8 +++++--- tests/rpc/test_rpc_telegram.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fb93caee2..037e40983 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -193,7 +193,8 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 - message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']} (#{msg['trade_id']})\n" + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" + f" (#{msg['trade_id']})\n" f"*Amount:* `{msg['amount']:.8f}`\n" f"*Open Rate:* `{msg['limit']:.8f}`\n" f"*Current Rate:* `{msg['current_rate']:.8f}`\n" @@ -205,7 +206,8 @@ class Telegram(RPCHandler): elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: message = ("\N{WARNING SIGN} *{exchange}:* " - "Cancelling open buy Order for {pair}. Reason: {reason}.".format(**msg)) + "Cancelling open buy Order for {pair} (#{trade_id}). " + "Reason: {reason}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) @@ -236,7 +238,7 @@ class Telegram(RPCHandler): elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " - "for {pair}. Reason: {reason}").format(**msg) + "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 86b978f3b..25b7e35cf 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1241,12 +1241,14 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'reason': CANCEL_REASON['TIMEOUT'] }) assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' - 'Cancelling open buy Order for ETH/BTC. Reason: cancelled due to timeout.') + 'Cancelling open buy Order for ETH/BTC (#1). ' + 'Reason: cancelled due to timeout.') def test_send_msg_sell_notification(default_conf, mocker) -> None: @@ -1324,23 +1326,26 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', 'reason': 'Cancelled on exchange' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. ' - 'Reason: Cancelled on exchange') + == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' + ' Reason: Cancelled on exchange') msg_mock.reset_mock() telegram.send_msg({ 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', 'reason': 'timeout' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout') + == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' + ' Reason: timeout') # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount From 02d7dc47802fe1ea80442d64f0410756e3415075 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Mar 2021 19:55:02 +0100 Subject: [PATCH 497/563] Increase cache size to be large enough to hold all pairs closes #4483 --- freqtrade/plugins/pairlist/rangestabilityfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index db51a9c77..a1430a223 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -28,7 +28,7 @@ class RangeStabilityFilter(IPairList): self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01) self._refresh_period = pairlistconfig.get('refresh_period', 1440) - self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period) + self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) if self._days < 1: raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") From 0b81b58d287cad3ffbd628ec221abd44f53b41ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Mar 2021 11:28:54 +0100 Subject: [PATCH 498/563] Use pandas.values.tolist instead of itertuples speeds up backtesting closes #4494 --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1b6d2e89c..bb90fedce 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -206,7 +206,7 @@ class Backtesting: # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)] + data[pair] = df_analyzed.values.tolist() return data def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, From 46965b1a2c9807c6f4521c1c3f9c9755f965725d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 05:28:11 +0000 Subject: [PATCH 499/563] Bump coveralls from 3.0.0 to 3.0.1 Bumps [coveralls](https://github.com/TheKevJames/coveralls-python) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/TheKevJames/coveralls-python/releases) - [Changelog](https://github.com/TheKevJames/coveralls-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/TheKevJames/coveralls-python/compare/3.0.0...3.0.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6ca1a4d9c..68b1dd53f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==3.0.0 +coveralls==3.0.1 flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 From 1f314f7d45e732db9a49d01ba827e3490f8d207e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 05:28:15 +0000 Subject: [PATCH 500/563] Bump ccxt from 1.42.47 to 1.42.66 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.42.47 to 1.42.66. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.42.47...1.42.66) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ed5d24be1..45a378d6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.2 -ccxt==1.42.47 +ccxt==1.42.66 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4 From a2b9236082379d17157d05a9585360a52718ccdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 05:28:23 +0000 Subject: [PATCH 501/563] Bump arrow from 1.0.2 to 1.0.3 Bumps [arrow](https://github.com/arrow-py/arrow) from 1.0.2 to 1.0.3. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/1.0.2...1.0.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ed5d24be1..24c2d2560 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ cryptography==3.4.6 aiohttp==3.7.4 SQLAlchemy==1.3.23 python-telegram-bot==13.3 -arrow==1.0.2 +arrow==1.0.3 cachetools==4.2.1 requests==2.25.1 urllib3==1.26.3 From a9c114d30196f9d9c372bc5f77ca5fbd07970a96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 05:28:27 +0000 Subject: [PATCH 502/563] Bump mkdocs-material from 7.0.3 to 7.0.5 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.0.3 to 7.0.5. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.0.3...7.0.5) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 73ae3ad29..22c09ff69 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.0.3 +mkdocs-material==7.0.5 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 7950acf6d4883092c8e68fab6e78a5cb104a2f9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 08:53:20 +0000 Subject: [PATCH 503/563] Bump pandas from 1.2.2 to 1.2.3 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.2.2 to 1.2.3. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.2.2...v1.2.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e1a8d26f9..3228cc89a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.20.1 -pandas==1.2.2 +pandas==1.2.3 ccxt==1.42.66 # Pin cryptography for now due to rust build errors with piwheels From 25c9e89956931c660d3656148f728ae66099cb40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 09:15:30 +0000 Subject: [PATCH 504/563] Bump aiohttp from 3.7.4 to 3.7.4.post0 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.7.4 to 3.7.4.post0. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.4...v3.7.4.post0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3228cc89a..f62c8ff52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.2.3 ccxt==1.42.66 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 -aiohttp==3.7.4 +aiohttp==3.7.4.post0 SQLAlchemy==1.3.23 python-telegram-bot==13.3 arrow==1.0.3 From 4b550dab17d8121dd79545f6d809f38e67f0a5f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Mar 2021 19:40:29 +0100 Subject: [PATCH 505/563] Always reset fake-databases Otherwise results may stick around for the next strategy --- freqtrade/optimize/backtesting.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bb90fedce..aa289dc2b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -174,10 +174,8 @@ class Backtesting: PairLocks.use_db = False PairLocks.timeframe = self.config['timeframe'] Trade.use_db = False - if enable_protections: - # Reset persisted data - used for protections only - PairLocks.reset_locks() - Trade.reset_trades() + PairLocks.reset_locks() + Trade.reset_trades() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ From 37e60061692e43e9094638de77328a65b2ffc541 Mon Sep 17 00:00:00 2001 From: Th0masL Date: Mon, 8 Mar 2021 23:21:56 +0200 Subject: [PATCH 506/563] Fix order_by in trades command --- freqtrade/rpc/rpc.py | 5 +++-- freqtrade/rpc/telegram.py | 8 ++++---- tests/rpc/test_rpc_telegram.py | 4 +++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 61e22234d..62f1c2592 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -289,9 +289,10 @@ class RPC: """ Returns the X last trades """ if limit > 0: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( - Trade.id.desc()).limit(limit) + Trade.close_date.desc()).limit(limit) else: - trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all() + trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( + Trade.close_date.desc()).all() output = [trade.to_json() for trade in trades] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 037e40983..759d40197 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -637,13 +637,13 @@ class Telegram(RPCHandler): nrecent ) trades_tab = tabulate( - [[arrow.get(trade['open_date']).humanize(), - trade['pair'], + [[arrow.get(trade['close_date']).humanize(), + trade['pair'] + " (#" + str(trade['trade_id']) + ")", f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] for trade in trades['trades']], headers=[ - 'Open Date', - 'Pair', + 'Close Date', + 'Pair (ID)', f'Profit ({stake_cur})', ], tablefmt='simple') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 25b7e35cf..924490821 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1128,8 +1128,10 @@ def test_telegram_trades(mocker, update, default_conf, fee): msg_mock.call_count == 1 assert "2 recent trades:" in msg_mock.call_args_list[0][0][0] assert "Profit (" in msg_mock.call_args_list[0][0][0] - assert "Open Date" in msg_mock.call_args_list[0][0][0] + assert "Close Date" in msg_mock.call_args_list[0][0][0] assert "
" in msg_mock.call_args_list[0][0][0]
+    assert bool(re.search("just now[ ]*XRP\\/BTC \\(#3\\)  1.00% \\(None\\)",
+                msg_mock.call_args_list[0][0][0]))
 
 
 def test_telegram_delete_trade(mocker, update, default_conf, fee):

From a1902f226d682ae795ef31cf154eebec62042039 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 9 Mar 2021 19:29:00 +0100
Subject: [PATCH 507/563] Make trade-close sequence clear for mock trades

---
 tests/conftest_trades.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py
index fa9910b8d..df9929db7 100644
--- a/tests/conftest_trades.py
+++ b/tests/conftest_trades.py
@@ -88,7 +88,7 @@ def mock_trade_2(fee):
         timeframe=5,
         sell_reason='sell_signal',
         open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
-        close_date=datetime.now(tz=timezone.utc),
+        close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
     )
     o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy')
     trade.orders.append(o)

From 99583bbd0ca9525aca9968740b4eda5f9e3da9a8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 9 Mar 2021 20:21:08 +0100
Subject: [PATCH 508/563] Fix problem with FTX

 where cancelled orders are "cancelled", not "canceled"
---
 freqtrade/exchange/exchange.py  | 3 ++-
 freqtrade/freqtradebot.py       | 4 ++--
 freqtrade/persistence/models.py | 2 +-
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 617cd6c26..e457b78de 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -1053,7 +1053,8 @@ class Exchange:
         :param order: Order dict as returned from fetch_order()
         :return: True if order has been cancelled without being filled, False otherwise.
         """
-        return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0
+        return (order.get('status') in ('closed', 'canceled', 'cancelled')
+                and order.get('filled') == 0.0)
 
     @retrier
     def cancel_order(self, order_id: str, pair: str) -> Dict:
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index f605d61c4..27c8bd48a 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -1023,13 +1023,13 @@ class FreqtradeBot(LoggingMixin):
         was_trade_fully_canceled = False
 
         # Cancelled orders may have the status of 'canceled' or 'closed'
-        if order['status'] not in ('canceled', 'closed'):
+        if order['status'] not in ('cancelled', 'canceled', 'closed'):
             corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
                                                             trade.amount)
             # Avoid race condition where the order could not be cancelled coz its already filled.
             # Simply bailing here is the only safe way - as this order will then be
             # handled in the next iteration.
-            if corder.get('status') not in ('canceled', 'closed'):
+            if corder.get('status') not in ('cancelled', 'canceled', 'closed'):
                 logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
                 return False
         else:
diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
index 3c9a10fb7..a4702fdf5 100644
--- a/freqtrade/persistence/models.py
+++ b/freqtrade/persistence/models.py
@@ -425,7 +425,7 @@ class Trade(_DECL_BASE):
             self.close_rate_requested = self.stop_loss
             if self.is_open:
                 logger.info(f'{order_type.upper()} is hit for {self}.')
-            self.close(order['average'])
+            self.close(safe_value_fallback(order, 'average', 'price'))
         else:
             raise ValueError(f'Unknown order type: {order_type}')
         cleanup_db()

From 60f6b998d3cc27599b3e95fa2da63f573d3727b8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 10 Mar 2021 09:27:03 +0100
Subject: [PATCH 509/563] Update logo with smiling one

---
 docs/images/logo.png | Bin 12030 -> 11025 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)

diff --git a/docs/images/logo.png b/docs/images/logo.png
index c7138e84b7e6f06a523e1d1a27b21691c6793288..8a7ffdd7091e40e0713c71967fa2b4bc32cad64c 100644
GIT binary patch
literal 11025
zcmZWvbySpn6Mg9J25A(eQ9wkx8>AcQ4rM8+r8}gN?(PO9MY_9}?gnZ2p7-x>&)GP8
zcF%8jo|!v$?wv3dC21^la&!Oyuw-Quixw)}hJJ>p#8atV@JASiFI}soU02)A6LR8&7{W!zJ
zi%|0ZMOZL?V3;9r8Ks|WIG0ug7Y`1BX|MFKmi1)a52d#92*)6C?K`x5gYNLamPnff
zXZiYz0WP6r^tMoU!8$5!ph$OOY#;+eyy=_x)PZ4$_sgC_dMd=_X86kNUH)2?)^GP8
zl_jMo-&`u%V#6&1Xl;h!rCd-vfW4EuU
zOTi|PQt8kvHfF*U7&J~yM&_YfHJ1f^AsX;>OKvjEkv>Oe>Q0W0RdHJH4zie3($LUw
z*4NjkWfO)fi+@8w7YsIlk^Iulo3NraRM*nd1L{?LoEaG0aHWdHR-l}WL8mc2!C0RN
z2ng~npFMSTf3gb-K4BKH{w`;0L;f7d!`wBubO3vL;_p#Y@CAvneX?`n~y;MSQOQTgJe|rmkXT$&EPunK`l4Yq|NW$HKybG-)sw>NXOmAmXvsp7D_gEDgRW$7`Fg
z6^e(eb&K(u5&G6()h38)=q2E#`(6rlv248OjsPHTM>|-{E-oh&`=bt!(6Z+zst~KA
zuK;XNHxZKQWH^bCau;-T!$)|Cu_C5%TQ|9+K`t&YQ+MhAKV*t-(UXhVd6Lt`I%4t}
z*kJ&s4;|?EDz3_2j6|JgKo8t9Dhazq(((5#Xvk6VNFDJhj1|3A@u}fDYtpf-VvzuCnqBzz2If^1Xx0BYJRu|0Vkq#L9?F0}wE=REjG!AC|$%MZ=Xh$iq$
zsIZ+%?*#O&Exb^S(yu4WEiLx;_R%V6B_EKGkdpIqbCtsC(*RCrgnc&djso15fHTI9
z{Nmzb=IQQ%kQE2FRo|B@%G$2aI!
zXeLb&kBp4WvgtKWJI+_bhA)_X#zKYHCdRdmpARvNUG
zqTo<@;V0|`;^EW<6kD+o4`rEMpzwnd@18l^Kf
zKVw~gQmnen8H60Uwr%YPF4(!Qem|`6z#n4d&p5fr5f6#1zbGv&jRX#m2HoGx_?maCyn%#GGpNxZHW8
z+rshdtlP(LEm`!VGl}XIEImgM4n_nF0p*C**qvx9{3pF6MgkItXTN^^LcX76zW{OZh!<8xn7pggc)+GGT6Oqey`27S-3g$n-}
zct$@_$*7_=ir|ER>Fo;=W
z1scxet^r~ZGH-KpCg|l^U0#uLmJ+3gR+S-}OEhx|coCJ)631#;C-xx)-==H6ZvM4t
z=Ldsp4~uqx&~s&n){A_S^;>#Z;rBwh&ieLkiyXnMaZm8)-5;1e2~XEmzAJ7!!>RI~
zHz#c5+*acbIjy5v!fX3#8Oh0v&6|+Rkq^|%jrL31-QC@T!Y_AY51o_~J0qXIU+tOJ
zKag^n{8L=wH2Id*z`A
z>YYGhg2ib19YRIG(&A!qn+T53Elj9=t-i65Ty#P3`_>QX@ZXopsqeP=3ZgY5db@4s
zz1O^NCZx`Ii{g#W$7g3PkOZa5Ira@(_?5A{2j*sm=;qLRI9-?%!@m<-g8}fio>O=e
zAK-eNq~{Q}{m^Dub&3RIUTY{UEL3vvV{mX?ao76*B4IzTGIWMHn*bAF7NxRbs`x60sJXJr`>LA-hFx>080f0Oc!ci
zM#et5HcfT)1R2yYM_XH4gI35s_V|;%V(_=(Y0G=Ri;|xo99Oxj#&*csy)b*ya~0wA
zi`7f{=$(P%Y~xlRv1ssBX1(3$;_L+a1@ldavsr)ryo>HZorJCi1qk$8>B1
zPt3v2&S>qjy}eDgd^!4)CxTayc27!bdHr3j>cH^u6)B#co}P!Wy>7)UlcuJtt1Hj8
zs=ky|@KZsu;h-Q0Zb8?Fb6E&;y!MOreyhG|Lf-r{jx!({oG8l6FEu%=
zw8nw`T5~kx(+9WV6t2(N<%5h0DNeF`hLclMCG@#7HrFW^3h8{GnMOs^RRqy9a-z$#
zeC|s^l$?*|Yl`kJdV71f!=uXenj8uw%m2LvQHwHX=0`#T;a(vEoXEe^HDAd+!$!M>
z?uS=Eto>4>$DS2fH{P<@l@!|te?-t8erOb2{6bpnFP#dbpj`33Ss9o!a9bGw(j;cf
zb*8MsPrp(~`<$yR`HL+G7qXnlvG^KUAAaIuG{gVIf9%1
zAI93cx)>j@J_-I@9M9L7PJ;(xc(G*}rvxLQU>BockxNW5L$YUVO1f=ow6$+uA|lII
z{)z}61rep=K(Xz3DNY4T)R_mEQ$C$E)3XT+ua(RlB9U-8XlHrTugN{UYBG7TR3Si^LFer_ozCwH~k7ZZ~lXH+($DlPA5eErPtF=4N+Wz13T+P1RU
z)zr{%E~IZ1NCZelkrT}=FAG?mZ}jfw=H{}WZ5xvo?wqup3hChJ64V
z!|?s}p`*~(jllE5G>3b*$5y4d*jOiv-f$w4!^14mG`!<
zd~zhEG-am}Drt_2;IKn_9^(IcDnCb-uVeObPJ0yhtXm@+bCMRj;;95`2vr*XKH!?8
z4@CUc*-DK26WKkcJTWox@m>g9QhR&jxM?jK!k2O_<{y6x*HuZ0!R^I
zuYt7y=ME~!8wFgR;<%{u%|4VXPwGOr3g90!qEnD`5bubc7B9Vt*pQL*!3!vooL{?xvVsi~=mz2MgkiBu8Fh|b?fzx}64L}~Smn}?Nc
z*i%KyIQ68TsXULk{`4ZbUdft`+5ko3*LrRE=#{UHT_%V@Le8oKU!ghuZ^6;T#N_Ot
ztO{8_x`}780ZLac^bWG8^6PFVO_b&3>1IuOtzF&dRUBSwU2p--4XJUWvl^^rH?v1g
zS@5;iS4vjxYT&))XhH!io;}ncNzPv!9=ZmQ5+4LZkba$4ZfSpM*@W;9ydM*wAMy`3
zCcRj+tr}Pmh)YXj*-Ae?uiQfwzD4r6O5c+pX>b;R&UJZcKVv}kT5~5|U2`!4XL@~~
zFR9}a61JMM{~a$Sd)=(K)i&9VWeSRQ-+$3^36}VdaErs=R7aPRSQvoO(|-ekr2jZG*Ns$FNHm@4w5)|O43
z{|>WL=d9`S8z)Oef%<`*E#Bi)T-ogb2&ReHA>!P8k~H=8^|4j$_u<2tf@#Z;DMhO8
zwAfgYzma6(V2>8(by${RCAN9Q(s93@(HcU$_nV7+hxmvNnM>CM-ur*Heg%}A^!XA;
zA~$noY2I9H4+ZeHPNHh?&8MWKhz&Yd1E`rNiu|S`F`>?QqL`a4k*aTv7Rk${>fc9e
zHpvz6+w!Ibe9Cu0Na8T^5rN$~;#^!Nqq9jZNC9zP^tV0NFkkU@L{7Nnn$SwGIxz)M*mi8Fk&(RZZ3hQrXw%Ndru60Ua_kb!&wNCbb4CUWYNsT7
zukDYW8L+b%>RH^(TFtO0rg2)g_xv0wLI|8g$1g>lL4}adA(iKaUMNt5Sl1YP!0KfD
z)0f4svU1s2S0ssY1tu*|)2IL!zHcLuzR}&J16#AzVvpp-6HX^Wtb=Je3dvNbH@`OA
zs00_2bVpy_<&rTH=QvwT>yzV&2R33J7op*T?(iE@4uvzVoAMhJwu#F;$h|LG%x{nQO^(sL~ZD
zTP6LGqZmcj0(Z|0Zl(-p=zBP~x=MYx^Cq*OHcaRNN7Vg_sycTvtaeGWXg6_vz@
zlV_+-C>Osl3VPKuhU0HmM969AI|6+D+((`sZiLT9Kb>xAX=w1sUCutEUVZoOI;`sO`D4Z1ILMl<4mQz6Znu+Vappw1Ql2!=d(4XkBeTl{uz}GO
zWY-~gB{urCJDGI8BWf#@sJr@cXIlkl|rb
zhjOxZa&m*u$4gCFhs0p>cp!B`YZeSiMEI?p*qTqZ8j=Oy(j;xNqFQbZDoaA+pF
zO5U>+xKlh^9EPbq&ibpZuAw-TB{;fs5EPT{Jr>r7ZQ;Dpflaf=@wd92u6lrx?;w?<
z-S&I>x8$fSEh&kHg!E5|=xq7mUvkz(f&s>Tks#KUpE1VXw@x>oQPYEYiMQ&kXc?Pf
z5|#wjYVdafve<5+<21NJbEEi+=AQ>)cO4@cWla%)haO
za<$2v);SRRA>RF>vHKXK%hN)Z!5*)r<%dAWT};2D{)byPCyW+iC-jXNn6)DeLm0uW
zF83xOGt(!#u`$U)W9Ar?Ksd2p@tqYXc-s0Vx!v7i8k|}C%h7nHcW~U2Y$K-2(qwgB
zEMT&_7$^W|IH*gatwrK6Esd5-rm@rKj($fAsIQ?yr{Z48`sCYXIHldUuEGs0n&o_i
zHtUzkO&~w<^xNGL6jS_nPS#2Q=Y#ajA?f2rxhI{;3tb|0+O>dadNjiCZ8``QA$^ec
z#_bf`D<5v~uQTu>SY|W~b?fvfaJEF}>oE{IfxEHBRkisLpqH8yHjU+!^_vp+Gpn?O
zL^|`^w_%FP$~f+QMHLkzK0Cu?YCb+b^0KmF4Xv#|c}OwwqN1Wek`@{hV+rzt1tIVI
z;$R%=N=hLwcLhyN%}A*{w0S3bP@y^j2gaI@8A^^%MIS1YpHs3l1fxd(%6;k_u@2fp
zagdG}(|*tcI)aKeqoM$xlf;th&WCR1^F`IUT@ZH@LT;kyB4Y}w+n<|myMWRZR{DFnWt
z-=C;2w%YXajr8?V?q9i53Sc=7{;HglDeqytzvK>dZQs!n@X&F}tblV++Voy|uy^Z%
zuAOz$8~8q5FFk(bOi4@A`}Cy~nztvAg)U_?G&(Azsjq)@$({<*_Nl2U-&mDC9hvt#
z#$DdV25R*o9v1>i`vgMwP@!xujL}aRo%>JTWW|pgy8=9XHL&VrPW
z!u0n?*;rU?Tb}O^3o2W;mHvVJk-DO!q~>nY0vz^!owOdp*LuQaJ>izo
z9YznlN3<|r)W_XgnY%`GejDXS2+h)l1qoom^tjkJ2DDHd@!@enn|1DJolg`$Pepa`3l;3~nwt
zBbQxve}7+^f}ETorWQG%t6a5&U7l$PS*h!E5_=?&+l3U)l4_45EStiqjS<6t>!RPY
zdo^&BQQ=i$U2=@2w)ZzF(G>5q%
z3L!?@6Gv_Iz!}3M#w@RI5;lDf^VRG4d=)lu9^d
zy4(^c&Y*f}4I?2ab#zcHtB=Dwh+b3rTRn1PW;h;TG#%=mosvwwdiAR_-tE25Rrn*`
z*mZP5YHEw3O-Q9~!vWo1ba$(1?bA)iOQIRx-&)qRpFicC)z{_3zUrdDr>taJa`C-x
zx7*DZL&{R)z6l8Q+PzhTT{(K0`BgIGW9AQWDnx-gjqLugW5<(bW!@WXr!vh?Yc7d$
z8falsJoxedX{&Kk^A{HvQmEAHT>ccYo|t*#ceKs^GuQ|({S7Ml3hhW~R-kySf#Iy16B%VU5I|dT6vesy}
z4Z$xN*I)&7gkAOWNIF7*Uj{f>N3=gG&O1#ieZSMW_;-YSvZbcIP
z^haaTzbhz2WWqj=*f)oPvDhSY{~cb=uM#Dt=Dz`q7U76QoH)fkdEKm%Z7#chpfvGFMCgyI!F(=v)ReYdSoLI(T^
zA5z0|e}8|Eg#Yu}cUyVYMr4v%jqq`Ma5t`)-ur4oI@fuow)Wk7GtJK)7>D3|wYJ?o
z$f=-aZ(mXDH~M&Y*~=@?%AQ+`(|>|kma*HZGxrPqL?AlC8CHmxhQRayw}=#MJo<9t
z)C@`MRbL7#Kgft!c#)Q53v#rYxD~9e4xt;vFyV?0+V~3ib0@2dWsc#Dkv!_ZG#QTIy$&0n*!ag85aYN
zO!fHT!QdDY2!$
zxwtg+UcZ2<)jj@GKEdH^1;OPh^mCpRn!Pa&R=!UQ8l!Q+$+~i;K*r{xV)YEF)be|oyoveqjj)9epl4T%kDz^?T3+hzeO!VfKKcnBS<*NXPSam9n~~CU!)4_<=hCs4zqRn!Q6R=WqRf
zUHXL@jI&ht)Z3q*Y;K7+-W3N9VJF3kq|k2oPk|pgF8Bo=_-kr^D}w|qND3R5-NN^n
zo8M0&4Xa|n;-+Hc!$Jp$OHF5@F--yOuM)Qv)VDDz0cI@f>
z2mt7D|Lp=Ou}DM)q0yG>W1VLrW0G^rFrkNyX;)~*rKWlWo9pO6(%-~eW!rEWVFdLZNJupQ0$HY|0%gBs3$4TW)aAI!C+obWk9KeB`1Hz)#=KbKBOdSAK32Zpz
z3aAs`rVq!ZYhL*o7dM$vv6=@CDH-ueNv<_HIXR{pLM{i>k)Z4GD1xD^MC>Ye4BXl0OoL#O|gECbf{h
zladK1X<(*-AYa|EgJ?4<3!tB4z~}j|92WQC{rmS{w!dqssHkubT5-F2iJpK6AmSD~
z4<_!U=3OO!P^N)B^m<5+5AD!nF$ff^rE;i2ujYU(xFo5BvP(d%ik*Ywv^s$t#6S*2
zPDhOrb@Tt+(9)vQju%5KL8q>@)&NmHG?wcw`-NIbh#{Ey8m}Fl{lyg&6#6XNT3Qx@
z0mJAT)4p(r6%dKaWMSV*I$Tf!obWXd0zP@&R_|Fp=Y&ul@Gb>&)6vn{pkR`YbKw({
zkf78%Up=h77*4xC^#7n2JMiBSIXE3S3z|RT<1nkyMqH^jYWddd->DRd4{U+^9Q->j
z+?+mQLQY|7_y@Y_qPq|*EUXG#q2ohi?K~PvP@rHr4KV2N@vc$n
z!xfVOwVTe62N0y5ym13(gdA4|Vv@5P8fN}-My^p|gVY$~Pi9}KMp>Z00e4=Sw?~gT
z3Ih5oBYuGt$5yqL@B5f
zKp^!`!4wWE!cSLI-j2V{rEoCH`al8HQ>>rCcd5l}0RQt`;>+W%aL}SRup(nm6#a;n
z@B?a`5&?GWASrw>&&){wuEky>0;%9ErBs8*qSAi55h14X%Titz)c&n(wn!
zMjXVa%uG36-^tZGO6jGe39;Ni8yPuTl1kY`GPdJ+q0s&*3iVax^Lcb%h@};Gy<1|}
zQY}$WB#w|g!w^TM1v6F51qpwN*(z`rR`c;Zx`aSo`C$rap{NMnCg#8b`*d%Qa-0%PRfmDVPFWBZ*{zwy!wfaq1qOB$G04X;1!NseIXb>1sb1OG;~pixj9go0RW&s=9KR?c
zAZX9Hi=#EX1$VC#jo^(P(XpVIjJUXXno?1`DfT{iMfMHFPzT1wszA0Y9`X^i8&_C|
zbgK-1>*meaY&*rv0Q~T&)1Pao4V?CIYMW2Li^!ElXkZt>yHLTLWzB(lStwQHFf5hyPrRQzBy-q
z{sDS*_sz|GI_i3U`QSbR0SUW4=iTJLjg1XCfQQbU+7Ew=jzIw$s$&2y<9rqu4=8?~
zp)8t`O=36D&DSg&3L3y&KT*uGs;;h13L;6W%-tKNBn{eaq)=3-VU_Ug
z{Jhma4Zw~!?UKXyPX4o*+1AGzet1Q2!($j6iy}b3-wV4<$I#G_1P|}4rkJ2IVg(5y
zLlI-gnRAU;ieW}k;O`lR>em-G$Nc9A_Pu+c0QzCGx|-W!1c~2(-psw{@ZB!|F5!SKVUR6+de0^v?eFtt>btkf)}nv!->u@LsQ~WbZy84
z+|Sm{hmwfE$noHd!B56Cntt;9ceMRlK56Uvc`t=P7-L3agK7c4
zB7w+{q`@UroL@w)|AY~6fi>p+{rh))aBuAtgn-aLeC(p68u5${bSepSgKQI60XR>w9EMMi)UOK)XH=$D1*0E;
ze%IJ4j7eg^)c=Bkup6ETN{0K9nP8)y2x_;~gFc7+5okwz%D2%f{0`l~j_b6#L@;KZuHj5C8xG

literal 12030
zcmY*<1ymeCvn~X8cMb0DED$8PyE|-fw_w5DgIfsB;_mM5?h=B#+ui*4zWd(Wb7rTf
zr>CS(bywF{6``ysg@Q+eVU{C&!
zu8FCGo2w87#Xm;>`}=P>U9BwtFDHAK|4|F9AnU(3tn4gotp7_J{3+mHuYi)Xl?9md
zKl;M#fd7pA|N8!i4}kR_^Z!Sf|JL+Byb#+BF_CRMw|y{SrYTB?)v92M(rz(qGJe8i&K+Ai1lUMP&5D
z2I7?0P|>7lv;8u~PAIy*|9IPI?^xmBY-{RxId2opo$~hZ7)?*-1Og|s_z$BG5Aw?J
zJ#S~En$~QpI?f?W9G+|l_#C%8Mzt0qU5xo*WqZ0yi*p3Z)QT02lm{hC1|ADjbggHs
zinH%Qs@sQNF39T9kXsLeB)+#Zsh>)uqjhrO&`1HDyRiEZ&dBBi-535JFMA_;9C?i?
zR!?UR{(@XK240=SP!?v}TOaQa*4cs{@5B%Xm>|}a?QD-to#InTE}0${h1sbLx4ObY
zi^n7-;TAn=90)V|QU9lTH6sOu?w!cfKGsLe
zVb0CNrniHcaPq}!!wCTR)yp(CK$eZoY%rRc&Vj1i_hBQ|SBuN%{ps-CzU|C%oR!c2
zWo>J*sz8XO!}c(Cvq$&-;-B1uJdfcum@;y#9rYgj2~sq>LE--IH@R;?hp)U50a@Bf
zd?UnARaOU2ONQQdlv&AIhAa40!!Ss=u5~nj5DXQ|anO5?K~?Vo6^~G80Au;@XAMiy
zSCRTI6Wl-@teaqBsAS{q{5WA<$eKAZW2(n#?nUiB|CR!Z%c2O@5
zRy`T->so%;EPTGdzk=+kgAyQ8_3Zimm#aVTv{)|Jn?M5>CJuY>E
z#U@%Uc;391pG2#)S!$hp@WUBdJrXij^AL(sw0wxfWBnkx*M9N(2|=?i)(7fdkaiK4
zxZcvvS+RUV;2LH5B1@adaXO=6gujO8WSG~u%i<*|43X`JZ3B|*a>q=yuE%D(7vn^(
zi2oC7*Q;}Bo_H9cn?Yfn49#d{g3QUI7A!}GmODvne@d00F);MnBL
zV)@=+jNne@*6W{*O#9X&BpwVLZkm40%H}G+JCIA`=J%7Mt+#Uz+D*TgvzHqT*9t?B
zi?l)zWVvF~`vI#zKk?Z|(_dE^32o8$6y~P&>xd|p*1y62Zzo1J+$gm-u_9&GPSdeH|*!wD&e`EVMjvh{cf6+oSswgGsp{!VA$?fY_n_k4S_
zmp66nd%$RP#YQ2&A1B46-#ncu=uye*@<$%+5O?$CqU&O(Zb6)tCuiXaRYdUPdW==%
zPf31kBC|HB00S)fyoEb>s^N}|iVlc->G0o8SPv-G3O(6)>*1B9%Cx>r+GnUu=jGg~xhNE1yR05HyJv-B}+^D)d}mM6w+_f7OR7
zcq)M91nabppuA0R^7W_H9fjE)@0QPvdSJ)XpG>sB(tC@pzB_-fwFNdX(Jf!`4lq9!
z^&AxPQ(KGs9A4HRcd4|0n*aF|28SZMTh;ac9W7Px?jY4@Gnd0cb_0Rk1}cR~XZNCM
z%TF;dWXc!9^T@d#mShff&6Y=fZkN;QGM2jRV_F=|qA{M*M~GHq*R66oqNlD0_~dU|~!OTQ2kK$9gT#jP*l={Lxc!p)@K
zO$nSZeLNlJdi=aU*xaCxI&fv|vH@X)FStqJJeR%$bA2tTgV97@r=J{
zL?6lgUu$qu=y!t9gfFkgRbL^X$8KqRlgH#^feOZmPWh5C94R<}$E&!T3EZ@AQvoWc
z(KnlDR{2ikWPXn(C);~LvO%^?z@V^*$hehA9811{lW-qwplsk)hqs4Ov2M{1`WO-8
zZU{zzV$)#5VU2ZJam?FZ8}%xJ0*zBVXh
zunGO4SmbR4(W5^cW6*h>>KijwYTRzqLdJtf*Nef(Wep2kN(=a8QgLTKK*IUbksPhY
zJj#PDK2hE}C-Qz3H^C^{FZ^<9#vHpomtn~mrrx6qi_<4fh=(e**~eN9x3jd%35Pat
z_y`3XdsWigHg3n)!ck-q<8#>q_h4bk%gewR&v0l%M-68i6T14`HNmsyDMjX6AC~JE
zM^qvtFCMLf!A!7+6S0`gt-zyN)Ghjhz7;(z)SqesCKtp;=@o>howCh-1!^-^5Di|f
zG7>?%Qm4>)-0@u5`_;2ovItcSLsTsxIdyW|kEL`f_L>x04`4ung6{yv)7JD?G;MzO
z^OR}7>_x)z3UpSjs)I48Bi)bs_zh>nMAJx-!|Un47sE?op?`@c{J1T>x&2jk8?ybe
zibt*Svckc4Oj}V_aL0Lf&JJUYhq*bO8i5)aw#W4dEgmYix}xl&
zz;VNYhM<9HD{Nz}wKt#RQ~*2cgNqPsv^y4IB(T8*<>(95$l%yfk+u6wLi-LLbBy?Z)E{ZZ7PFwnC+2+NBizc
zVA<)ZzMoyV__9#thZBKhs4VT|$2NMntCq~N1^w7Lo6kKQ7QnpxXr}jmkGb_ZBAI$J
z+(^%dkMp%EEQ^C!|9B(g2THLiKV%_53mm1i66%D<%<8kJDMQQ
zI{X1uraak{ltlHS8)~fW;o)WLqw^st_qBfodb~|Yu`J8WHsbPdGtTdpQqP%P*v^g4
z()L$amNrTdb;2C@40=$bB6pb`dKyA^)oW`FaK#1GKvt3);`Ver?2!Q#39!+
z`J8?uxrQl@MJ%wl?+wL0ykDnwd7h?_v;Z8y`@}6@^=1EsB?`s2ayubxB>=Wf)&O*F
zz5&_<9k4St=i3&c$88Sqlx!$A=Y1c5;;VCURGfQXpDBs_=kw~7veIoc7CB|TInRDx
z&O0=jh!&|-eU9QRRm-#}T+SyX#jQl@Oja7&#b~qGx8&lk3m>V*umv>8*9+eZ?*+I`
zQSv{wU*HIaE&ob}k!!ZUaQA61l*c`ivGs;l*=uoBhEiz81Lu#MKi*%2Vnt)%rzkh@
z(Kq{PGe1eSe(($|F;?bzYCg8&S&paG_!8@^UjDk=$+O+fyVbv>A(=|*`apxymi5$L
z`i@WfJ&8u)+LqQ?@S^=j&Hpu@1vDZ)g6|35M+(-h)LVx!Swmth5m>%nGc{98Vg4Lw
z+HTqi4$aU}!SIn02BjHzRKUTy6|q?0MpQRTS2)ss;a>Y3g1MnWPK})%EaR$C^<(I4
zE#YI7ffDV&6hg3MLb)z!&}D?vwuO^XLYED5{FtR9t1*4__F`*mi3I8(Sk_V#+fO+3
zt9Jf6`{-B*+a-7bnTYV{Q9mJ|v;Pz?AOD53_5?b2{z$X)rB6yF9HrJ|LJ-aXJ8@GZ
zbTR*TZ3bAjmSxP1*;KjDfvOQ2oh1bl2C^g#qc)si-|EEQKMH%lvR>ApF3a>;1h9{>
zj_AqxoLQUW@{CEBtX*d)pu+eGrEdXQ(CgZq$c|>06Z0ey>koAp
z2Ow}iZMa&*p9My#hT~L#+}ZO(5j0Wxc8fR=80=ykF-{bCH(cW{B{bVY%nO55g04}h
zQKV1&<4A>Y6M>&CCvbAx^_>QACXWlc1DC$oJSX}rMN~8~{smqG{npUy5D6%5>#LfN^CV@BY9vg`Y6uO9?#IxvL}q{u6dT0L
zilVg=?a6Tk=IasYW10BTDW+C;y4vtd2_@wm=iBQ~|ZA|jHK5@nzHPai3
zd{bxkTR9^X`gH{8(s4DqYBl}JocLp9v7bkFy4L@+X2>vFD^n0Z$zpeb;-kWsh+SMb
zH8Pt+7G5(}(Pl94vL?E=0kTiH%smVo`^~PtgQs%JpqX0(6j6%eEc%n0*kzo3@R~Tx
zlKy-Rm8&{28xErB$u7&e>{anCTSdfYI+$#rW+bZ*hKfk5WtEPPMT*GW<_jQaj6OCd
zrE-&qW%l50l5g2IUKOrwCgKhptzNn9TJ%B^HMKI9?DFoYtF2)V!Jb%>XkY5LLBP0*
zZNK4iOT_mh*FiJWKix$1@2MSk>NZGn!nwaj?YSKraFV{(Cm9o^Yr74^>yKnUSMs$_
zL~l3QL-w-p$UbNI7U^N7F#u|WV1q-
zqOwt@RnGd#gm>&LVuchVTYeqSXFiN>maF%&rFvdIaNaIBD!z6c*Vf6y;Bm^9mmua*
z5OZ0~sfTki29w!A46lt^xYNHBNR-3*iLN4&W+Evtf!GFcTeh7|2uGchs2iTI+
z9}!ZOPDe_Zpl#&$WZ}Q%`@ntA{g;>1e&k&<;iS^HA45t$T!|;sq4P;Ct)E!^xLfFc
z*$gW19=a>7X)7K^QKLv7!9|Jx;Ad1Xj`P{n%kUIWurv~;JN^qRE~}ybx_aJld$57=
zm5_d;?$l**>W=-PCtmyAlF=3spQG^(d=khF@jOr)1#weV{$Rfo&Ptp@2KP57aFU4+
zWQ?VGIh{Bx0WIlC3UxLDnYC^?%F(^AYH-fF(}6|)lh-kX{_3auo4`UKJRr4?J7pzA
zUvjBQDOdP{>W41Hw3;@QQ1GEjvi?xwS3m5^Vj=M~A1?L+*mkEq%cgB$Q(Ef8Vt#$s
z{UVvJ=A|}mVfe6?(D&QFA4s}E%g3NASJ>k!(eD+0>FjNy*c{$5^%V$PdX5vq1?qsh
zsz(=G{m(^NzZbZXe3m0LmX)O8^@*`)Dnrr`??zNRe*@S~`}%NB(pPp#;0tXa@cmW1
z`){_@A0o*+Qo43Sl!aw%FL%$5hA$qSJF3-*qj;HNXv0U(f#X=$s%jnjG|p1QJlBgL
z1uqb8aaHhL
zvX%uuG#Bp$y9ck+BG+Q2OR+fdimSuS_>rwquk>(+824?^3-^l!=Q~Buu_LB5p9BE#C(mZ+yAax;2wb?x;{U@voD|J$HkjT3;dtPOtJwd
zC`NYm%X-~B!Bv2
zxLgmph=*ptCu*rmo57ofXM-{9)&i+s+~HC`Xcdyb9U%rERQ8cNRhT}8-;WB12Aq2L
zT}2_1)-b*6K4;@h<$D}=!RUJ+Txa+>?X=|Qonn@Iy<}c%VGp&OQ(uiXCp_s863p-#
z)3yMNy!tVdNsh*FGz?8l8dP|sq>(C(si$GfsW($4zo&j7saY^MC7YCV^0`BtQKPe`
zps~~J8w1RS7)tX}8A$s))*nvtG54>96fGF-TC`w2oO^agZc=+kjcurx+Ly&bN$&Y+
z6^4AUU5-&{MfH{43l5(`i@eG8TPJK-HCO254ET%QcAn?VQupv7+}F@tR<)fA7&Y1E
z>!?2-@U@~>xpGY-rXso=KhM<^wc8QUennV)m2J1v+2r@V*=g}%slDX)xlF+0q&a}wpAxoY61HFS{psVQ
z{z&mSI!kkqkXPX8k^{ozdk3I=NoZti>y%WYBnxqj2#itL?HzDP8aDcgJV0dmo0`Wu
zY42(DJDyU2K9kdVB}op#whV1HSLma3m;z^YscpsXEhKxUAFhMes$gZ*t4hA))B1?6
zu!SWZg2;u-^P!6;dXJv&%TJcyCNv^H4kMvvbL_XRgnv++bvwIH@i`KA*oE{qCxm^K
z_tF9QU)tXor5t~Q`nZQ07}|l{r^<}SGvi_2Nf_L#^kFbFHZOTv&5b^+8u}wUoEIb4
zH0nWl7T3i3irThkMy&0)XC{y_@x|K$sN$P-`WQy~{o9}WvNK)TmYIVlfEKYf%oL<_
zf#0Ba212z&FflrTfuPez>iVdJfb+ChBE(4a^E_$1~gC_+s=~pVp;Z0kQEM1!7VW@Zf
zV$7M#Wje}r%w!oFcT@AM^N<%RU5q`9pkaY+u_ki7D=JcwxugyS#{+t}KyF#RX831k
zHGpCh
z{``i3L{BDIZv&b-XOk-3YU8r(nBzZb!`&iO*2%5`2Y%05nGqd
zNMK_}6swpcfDe)mo85M~`eyIos`d3uIi;VHC~a=fS*K@umU6990#YitoaSh*OI`R(
zNvl4rD!>6ca6vK-hrPk!STM7Jz~yjKp)8935>GL4ohw${{;ZQDi}oGayBQ8tdz*q~
zC*jIVkwcc08+_X7_PVPhq?F0fk|bg1sRx2F|Bb1^B-ZhK44QbAfM*(8z&=3x*n#)V
zR8ASDObqIG#$o{-{zLJw>CL?Ymh_Ves5Q8#*%JV_N4odc&{xeR__89ux92o==~fJm
zv;=)dj4a?%7E%~16fiSo#4v`EjmR+qL#CChWTjSE}b6k=oi
zfBd%0f!Mb9hoLD89)AI2euC4w0hoW)wRjAqP{ANG&Cf5EHTzS0&TCrfy+e88D;qk^
z+!zKkS}+WcNu?1LF6epKdXZjsQ_rey^Kz}+)AxmX9_5OjX{2h#_
zYLUy1NQu_h2@?{_X!$8R`Pe4<@T(>j`L{rBeWWjc%I;E=N-%b_9p%oX9AF^7e^!3r
zA&2Bc!#j(Rff;g4^p|{w=y@9#K(9bp{V}&ROvC)6&)Q3~xm_j-4fpQYk@S!=`=s9}
zc*qf#T!oXUpI~PAc&Td3rN~*orITWk67v<&i!vNxbIwxBsu9l1u8a8T*~isl2G*x_
zioazF)512lVzS;7z6b*EzOdZA##&}FW3>pJ5plz|yE5O7NPG<|a<{Tr5DpDxs%S^7
zy*i}*M0S5PC}uh;;>8kdyyxQpiA(c5u%Mc0VreTA9eMXHDeY5dt@$znLQxdXF@eah
zrLU{W@3D9yWGTf|f$<<2=8JDFQmdF@
z1~DF2hiw;~-a2){oEAHOc`HcTz!3Y-dVuUwEU{HDA;6}=fTof(JSg%oIfc4-A1svGEpLoWshqO|2I_}F+#Ie%$j50zW
z(Y_81po_Uu{B@mZT#s*o$Olg4P>(4dEae_>&W1u5R`{Fr`AqB=LPuY7)IjU3vD~A<
zJR4Be#WS&~y?6P*8ZX4^2SgDhdYw%@7*ge3$djL|p`)UW!)7kWVe&O5Gvf
z87YE!&xos9kITrW1K|U|44Wb!xWns)28Q9kNs}syVa7P%@JL;Uj9G0{DNhl`Ge5U7
zQzPeZPMM03FKB27VWe78W@O2E*-!4oa!HpXU+XeDyk<)W5
zJe+UnB?g#6?2P?>yoAKSFgK0jtVxRq(=>WW08mQB0w}ZKigEPB_irMi_K~_c@pXol
z!?y0NGI2EV?e#<`)32V=I3K?5*4Fcg&3h*l$r0NSiYj#iBb!z%?!*{o|g#^l?cuvj?J&^_Y>nwaI3w|
z@T#O6j#Vm$R*eV`;VKJvAWtII$GBt)xf_UQ4g94U;GfqVdm;ZC7puK@>4uCKM(y>h
z0EN8X2GpusQ0eFwk;(=$#Z~#&0atOf_h9X$jtaI~CH;BeszI%wP@I(c{xuIANK
zbK4euj)A}=2dW;aes&_QFOvKw$|7yc@=)Js&_
z+;^xDd%U5GW~ap%3-YW9WH@_935#RQaCvjYpjEMf9o*a@MdD{1-vo+HF#5?gzmveM
z%gJZ(!IzM^x)}!oYE>i9=GJLW%$1(<@4Z`ms?@7!iW}|>6Qi6Qc6a{Cn7=<
zDI#6}d;#6dl~OZHzaN)DVf|c_cBmxRIz=C&y9g7j9
zUKFREg1+M(mYa~%j&Lrks>=XG9-~)NCZ;Pk)+OjPtdLoAS>;0Qc~>dv!tr>{qU{Rz
zeWR<-?+;5MR(=-#G=OQd#-8Bb`_o3+(l2gL9Vo0v;mf%GDcqJV5yp3Bx2=+VH$TB%
z{4?rjZ*~PV(F~TbAd5@3Blp(wXza%JzdVG#k4hGrdX|ZU0{dlmlp}hZTs`JIJ&ukuo{O6kPjl#|
zar1U8!T56i+A(?x`bh9*Vw`8i4PUi1O?^-rI1}$Ft=O(4l6-LO4<(EnIF#tGvU#=b
zpM9{sQE^neT}`t{pO+V|;Rm6;8%@FZ$Tbz>vy++*Qv5cl$^4dN7H?|Fl^r>C0O;Nl
zjLOrsvf!>RdDI0Zhhjj2kDen2R}L&0*YlyZ)IzjdL-)VK-jKk!|yBiim&vg7Yk8YyQo*h
zAN+!}W3zGF2BmV>pdz4rEnoi)Y#5L+gCxJ*wNfRy%xaO9feZYFMZfgbR#bSciV8Wr
z+IA$w^VM5tZYMvrfoAbDSS1`I^;$wUSQ3ZUPpcA@`|b%eVh663%eZqWK?D8KXqP#Y
zS)Lz_ML`Zt#M8R9JpcHNb6~ubv&r(gA#~B?VARv@uBt&JQjrR0w>ygP#ekY4^uu?8
zcTAx!hb(g4vz2Xg*fR4LId0KKU3YsXTz~y@eF@aE}qJY#24nnesT|gd%fZnyBT%
zr61l(XmF$<0J<+@zQtkeg*Ebv!(6xn1NO$;FHVc!S1EFR%dDeT<;wFHOc%8oVrbn)
zuwo$A_*A8O)VXqk{6y_sIU2H@Ymja{T$)BGe?S@Qi3jIZyx4X
z+;c_vAX&f(-J%onMj+gEF;>Vi{)}=zBHA4p52@w~S?5P~2{D`T56+Iw2!U1i^*kpTAj9&72h@E2EnM={*T$Ssj}WhaRo<|uTeL4mY)>Y{0$EoCl<96=*vqFkUs!2Fg&jqK-xQh%-04nlmm(9&
zvI@@U6p;%gKMI%kwcQ!OGrDb_T1YwV3AFvOeB#f;vzX--hy#_zz&b`hC1uW*fl-`p
zHa4%LrL{T=j6lmY4O1uvT`@H!KR#BK_?DBFDkKbv>KUn+dmvTNr-eo18?#kWgD_TF
zPd`r>8s_}AJE-eVb-s~fSP14Y<~*P@1eMr@xBB#ZelGbVwV`Ko!boeiCu8H0x3dgk
zqO4`UJJ3JsxI;+mR=QM~dBQVBi0)VLOaT;z{%Y>
z-!w;2tj1XfHO_{~At)1<%pGxQF{f&Q=P5J9AN6kbH9R}DVTXT1u+k3t$Y4f!1xi+E%!~mS>X=0qR6hml}y@
z_z7|H3qw<1R+pbz^Kqh7DUpe6^#GiDHNq`f=AjI|F>>_1Ry>|MdkcQZ6j5$#q`_!f
z9SYufxZ#yv-#P>toi})saS^DZ;H99(YX1`Vv&k=UI?ItwynL!~#M7=JVF)r5XFm%i
zvJWq4F)k`R%8&LY~^uak;@5G
zG`*C^NJ}tpy8hA>4ME3?gjR5Ww|)w1YTA{wld|;X@ACSFg#3$eIX7K0C2dg@11+8V
zFN;o9s#7pZO$c2`i|5a$6}PI)7nYF3&6#GlG5a-
z#dIl_Xmdr14|V;qy%F);-CM>aBK1eaP2@tfvAg>m+bV1$nRvJ3zccwAbyLEE3z)bk
zn?o%D7UYQu`k*q*f~DU~EFqevElU=kCJ8i;=*)n#Fx(T?cah>&YfH@AI?qEjx5x&w
z`bdC7Hc&9+tyAOpi*jB*AwX~
z!wy4~aE>#)DP{}1&eg!-hBYAn6jx?h?jx!vW~zkBOQtS`V3+|vEO~)_7KBhaQ$xFG!yIQX{(YH!sZ~kDOAqe31m!fJH;xGk~VK1qGQm=wBGez-h|@T}i(o
z8ygkeJCwsP$AE5MtXNe2)9!=x?=kmdj5g~ZCZhZm5lX0Bf_Z@HawUVwOU`YQG!A7W-v(O#b7HT+Q^^-V#$mbQE!n+X1dGRaWF2%~?2B(IkAooYUGjEsd2;kM-Muw<8~G)3qq}Tjr@%QFb=(`_roJ{O!!YX!3zduN3mq4c{%z?g=*4
zXslC!lxax!qONLkP@_+c
Date: Wed, 10 Mar 2021 10:39:38 +0100
Subject: [PATCH 510/563] Fix random test failure

---
 tests/rpc/test_rpc_telegram.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 924490821..27babb1b7 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -1130,7 +1130,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
     assert "Profit (" in msg_mock.call_args_list[0][0][0]
     assert "Close Date" in msg_mock.call_args_list[0][0][0]
     assert "
" in msg_mock.call_args_list[0][0][0]
-    assert bool(re.search("just now[ ]*XRP\\/BTC \\(#3\\)  1.00% \\(None\\)",
+    assert bool(re.search(r"just now[ ]*XRP\/BTC \(#3\)  1.00% \(",
                 msg_mock.call_args_list[0][0][0]))
 
 

From ef9977fc1e8d0cafb5de9fd57c93103e2bb9eb37 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 10 Mar 2021 10:43:44 +0100
Subject: [PATCH 511/563] Make stake_amount + stake_currency mandatory for
 backtesting

---
 docs/configuration.md                        |  6 ++----
 freqtrade/configuration/config_validation.py |  2 ++
 freqtrade/constants.py                       | 10 ++++++++++
 3 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/docs/configuration.md b/docs/configuration.md
index 823b4bc20..20b26ec13 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -40,8 +40,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
 |  Parameter | Description |
 |------------|-------------|
 | `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).
**Datatype:** Positive integer or -1. -| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String -| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Positive float or `"unlimited"`. +| `stake_currency` | **Required.** Crypto-currency used for trading.
**Datatype:** String +| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
**Datatype:** Boolean | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
**Datatype:** Float (as ratio) @@ -142,8 +142,6 @@ Values set in the configuration file always overwrite values set in the strategy * `process_only_new_candles` * `order_types` * `order_time_in_force` -* `stake_currency` -* `stake_amount` * `unfilledtimeout` * `disable_dataframe_checks` * `protections` diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 187b2e3c7..df9f16f3e 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -47,6 +47,8 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: conf_schema = deepcopy(constants.CONF_SCHEMA) if conf.get('runmode', RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE): conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED + elif conf.get('runmode', RunMode.OTHER) in (RunMode.BACKTEST, RunMode.HYPEROPT): + conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED else: conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED try: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 06eaad4f9..f25f6653d 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -378,6 +378,16 @@ SCHEMA_TRADE_REQUIRED = [ 'dataformat_trades', ] +SCHEMA_BACKTEST_REQUIRED = [ + 'exchange', + 'max_open_trades', + 'stake_currency', + 'stake_amount', + 'dry_run_wallet', + 'dataformat_ohlcv', + 'dataformat_trades', +] + SCHEMA_MINIMAL_REQUIRED = [ 'exchange', 'dry_run', From 425cd7adba609b810ad0a0486925c4585af42b76 Mon Sep 17 00:00:00 2001 From: Jackson Law <178053+jlaw@users.noreply.github.com> Date: Fri, 12 Mar 2021 16:16:03 -0800 Subject: [PATCH 512/563] Create event loop manually if uvloop is available asyncio.get_event_loop() does not call new_event_loop() if current_thread() != main_thread() --- freqtrade/rpc/api_server/uvicorn_threaded.py | 27 +++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py index 1554a8e52..2f72cb74c 100644 --- a/freqtrade/rpc/api_server/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server/uvicorn_threaded.py @@ -8,12 +8,33 @@ import uvicorn class UvicornServer(uvicorn.Server): """ Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742 + + Removed install_signal_handlers() override based on changes from this commit: + https://github.com/encode/uvicorn/commit/ce2ef45a9109df8eae038c0ec323eb63d644cbc6 + + Cannot rely on asyncio.get_event_loop() to create new event loop because of this check: + https://github.com/python/cpython/blob/4d7f11e05731f67fd2c07ec2972c6cb9861d52be/Lib/asyncio/events.py#L638 + + Fix by overriding run() and forcing creation of new event loop if uvloop is available """ - def install_signal_handlers(self): + + def run(self, sockets=None): + import asyncio + """ - In the parent implementation, this starts the thread, therefore we must patch it away here. + Parent implementation calls self.config.setup_event_loop(), + but we need to create uvloop event loop manually """ - pass + try: + import uvloop # noqa + except ImportError: # pragma: no cover + from uvicorn.loops.asyncio import asyncio_setup + asyncio_setup() + else: + asyncio.set_event_loop(uvloop.new_event_loop()) + + loop = asyncio.get_event_loop() + loop.run_until_complete(self.serve(sockets=sockets)) @contextlib.contextmanager def run_in_thread(self): From d1acc8092cef3bd2a2035642640a7cab92429dcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Mar 2021 10:16:32 +0100 Subject: [PATCH 513/563] Improve backtest performance --- freqtrade/optimize/backtesting.py | 4 +++- freqtrade/persistence/models.py | 32 ++++++++++++++++++++++++++++--- freqtrade/wallets.py | 11 ++++++++--- tests/conftest.py | 2 +- tests/test_persistence.py | 2 +- 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 575ad486a..f2cf0d0dc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -377,7 +377,7 @@ class Backtesting: open_trade_count += 1 # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") open_trades[pair].append(trade) - LocalTrade.trades.append(trade) + LocalTrade.add_bt_trade(trade) for trade in open_trades[pair]: # also check the buying candle for sell conditions. @@ -387,6 +387,8 @@ class Backtesting: # logger.debug(f"{pair} - Backtesting sell {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) + + LocalTrade.close_bt_trade(trade) trades.append(trade_entry) if enable_protections: self.protections.stop_per_pair(pair, row[DATE_IDX]) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ab714ae8b..41a5a99ff 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -208,6 +208,8 @@ class LocalTrade(): use_db: bool = False # Trades container for backtesting trades: List['LocalTrade'] = [] + trades_open: List['LocalTrade'] = [] + total_profit: float = 0 id: int = 0 @@ -350,6 +352,8 @@ class LocalTrade(): Resets all trades. Only active for backtesting mode. """ LocalTrade.trades = [] + LocalTrade.trades_open = [] + LocalTrade.total_profit = 0 def adjust_min_max_rates(self, current_price: float) -> None: """ @@ -599,7 +603,17 @@ class LocalTrade(): """ # Offline mode - without database - sel_trades = [trade for trade in LocalTrade.trades] + if is_open is not None: + if is_open: + sel_trades = LocalTrade.trades_open + else: + sel_trades = LocalTrade.trades + + else: + # Not used during backtesting, but might be used by a strategy + sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open + if trade.is_open == is_open] + if pair: sel_trades = [trade for trade in sel_trades if trade.pair == pair] if open_date: @@ -607,10 +621,22 @@ class LocalTrade(): if close_date: sel_trades = [trade for trade in sel_trades if trade.close_date and trade.close_date > close_date] - if is_open is not None: - sel_trades = [trade for trade in sel_trades if trade.is_open == is_open] + return sel_trades + @staticmethod + def close_bt_trade(trade): + LocalTrade.trades_open.remove(trade) + LocalTrade.trades.append(trade) + LocalTrade.total_profit += trade.close_profit_abs + + @staticmethod + def add_bt_trade(trade): + if trade.is_open: + LocalTrade.trades_open.append(trade) + else: + LocalTrade.trades.append(trade) + @staticmethod def get_open_trades() -> List[Any]: """ diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 553f7c61d..575fe1b67 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -10,7 +10,7 @@ import arrow from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange -from freqtrade.persistence import Trade +from freqtrade.persistence import LocalTrade, Trade from freqtrade.state import RunMode @@ -66,9 +66,14 @@ class Wallets: """ # Recreate _wallets to reset closed trade balances _wallets = {} - closed_trades = Trade.get_trades_proxy(is_open=False) open_trades = Trade.get_trades_proxy(is_open=True) - tot_profit = sum([trade.close_profit_abs for trade in closed_trades]) + # If not backtesting... + # TODO: potentially remove the ._log workaround to determine backtest mode. + if self._log: + closed_trades = Trade.get_trades_proxy(is_open=False) + tot_profit = sum([trade.close_profit_abs for trade in closed_trades]) + else: + tot_profit = LocalTrade.total_profit tot_in_trades = sum([trade.stake_amount for trade in open_trades]) current_stake = self.start_cap + tot_profit - tot_in_trades diff --git a/tests/conftest.py b/tests/conftest.py index 498d65b0a..801ffad2f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -191,7 +191,7 @@ def create_mock_trades(fee, use_db: bool = True): if use_db: Trade.session.add(trade) else: - LocalTrade.trades.append(trade) + LocalTrade.add_bt_trade(trade) # Simulate dry_run entries trade = mock_trade_1(fee) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1a8124b00..8c89c98ed 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1196,6 +1196,6 @@ def test_Trade_object_idem(): # Fails if only a column is added without corresponding parent field for item in localtrade: if (not item.startswith('__') - and item not in ('trades', ) + and item not in ('trades', 'trades_open', 'total_profit') and type(getattr(LocalTrade, item)) not in (property, FunctionType)): assert item in trade From 5e872273d1217e43e15ce91317f9ef1f68f36277 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys <19151258+rokups@users.noreply.github.com> Date: Sat, 13 Mar 2021 09:45:16 +0200 Subject: [PATCH 514/563] Provide access to strategy instance from hyperopt class. --- freqtrade/optimize/hyperopt.py | 1 + freqtrade/optimize/hyperopt_interface.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 16a39d7d6..6b5bc171b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -73,6 +73,7 @@ class Hyperopt: self.backtesting = Backtesting(self.config) self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) + self.custom_hyperopt.__class__.strategy = self.backtesting.strategy self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index b8c44ed59..561fb8e11 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -12,6 +12,7 @@ from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict +from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) @@ -34,6 +35,7 @@ class IHyperOpt(ABC): """ ticker_interval: str # DEPRECATED timeframe: str + strategy: IStrategy def __init__(self, config: dict) -> None: self.config = config From 0320c8dc92d4e4acb4f61310f202ba6db2be840a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Mar 2021 15:46:20 +0100 Subject: [PATCH 515/563] Improve tests for trades_proxy --- freqtrade/persistence/models.py | 3 +-- tests/conftest_trades.py | 4 ++++ tests/test_persistence.py | 22 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 41a5a99ff..ed8a2259b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -611,8 +611,7 @@ class LocalTrade(): else: # Not used during backtesting, but might be used by a strategy - sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open - if trade.is_open == is_open] + sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open] if pair: sel_trades = [trade for trade in sel_trades if trade.pair == pair] diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 1d775830d..8e4be9165 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -29,6 +29,7 @@ def mock_trade_1(fee): fee_open=fee.return_value, fee_close=fee.return_value, is_open=True, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=0.123, exchange='bittrex', open_order_id='dry_run_buy_12345', @@ -183,6 +184,7 @@ def mock_trade_4(fee): amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14), is_open=True, open_rate=0.123, exchange='bittrex', @@ -234,6 +236,7 @@ def mock_trade_5(fee): amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12), is_open=True, open_rate=0.123, exchange='bittrex', @@ -284,6 +287,7 @@ def mock_trade_6(fee): stake_amount=0.001, amount=2.0, amount_requested=2.0, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5), fee_open=fee.return_value, fee_close=fee.return_value, is_open=True, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 8c89c98ed..ab900cbb8 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +from datetime import datetime, timedelta, timezone from types import FunctionType from unittest.mock import MagicMock @@ -1044,6 +1045,7 @@ def test_fee_updated(fee): def test_total_open_trades_stakes(fee, use_db): Trade.use_db = use_db + Trade.reset_trades() res = Trade.total_open_trades_stakes() assert res == 0 create_mock_trades(fee, use_db) @@ -1053,6 +1055,26 @@ def test_total_open_trades_stakes(fee, use_db): Trade.use_db = True +@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize('use_db', [True, False]) +def test_get_trades_proxy(fee, use_db): + Trade.use_db = use_db + Trade.reset_trades() + create_mock_trades(fee, use_db) + trades = Trade.get_trades_proxy() + assert len(trades) == 6 + + assert isinstance(trades[0], Trade) + + assert len(Trade.get_trades_proxy(is_open=True)) == 4 + assert len(Trade.get_trades_proxy(is_open=False)) == 2 + opendate = datetime.now(tz=timezone.utc) - timedelta(minutes=15) + + assert len(Trade.get_trades_proxy(open_date=opendate)) == 3 + + Trade.use_db = True + + @pytest.mark.usefixtures("init_persistence") def test_get_overall_performance(fee): From 6389e86ed68c42425c456fe3c8373436be78f71c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Mar 2021 15:36:34 +0100 Subject: [PATCH 516/563] Add test for uvloop fix --- tests/conftest.py | 12 +++++++++-- tests/exchange/test_exchange.py | 10 +-------- tests/rpc/test_rpc_apiserver.py | 38 ++++++++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 498d65b0a..75632e4c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from copy import deepcopy from datetime import datetime from functools import reduce from pathlib import Path -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock, Mock, PropertyMock import arrow import numpy as np @@ -64,6 +64,14 @@ def get_args(args): return Arguments(args).get_parsed_arg() +# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines +def get_mock_coro(return_value): + async def mock_coro(*args, **kwargs): + return return_value + + return Mock(wraps=mock_coro) + + def patched_configuration_load_config_file(mocker, config) -> None: mocker.patch( 'freqtrade.configuration.configuration.load_config_file', @@ -1736,7 +1744,7 @@ def import_fails() -> None: realimport = builtins.__import__ def mockedimport(name, *args, **kwargs): - if name in ["filelock", 'systemd.journal']: + if name in ["filelock", 'systemd.journal', 'uvloop']: raise ImportError(f"No module named '{name}'") return realimport(name, *args, **kwargs) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 75db2de26..3fd566fa3 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -18,21 +18,13 @@ from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import get_patched_exchange, log_has, log_has_re +from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re # Make sure to always keep one exchange here which is NOT subclassed!! EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] -# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines -def get_mock_coro(return_value): - async def mock_coro(*args, **kwargs): - return return_value - - return Mock(wraps=mock_coro) - - def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8590e0d21..01492b4f2 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -23,8 +23,8 @@ from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.state import RunMode, State -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, - patch_get_signal) +from tests.conftest import (create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, + log_has_re, patch_get_signal) BASE_URI = "/api/v1" @@ -230,7 +230,7 @@ def test_api__init__(default_conf, mocker): assert apiserver._config == default_conf -def test_api_UvicornServer(default_conf, mocker): +def test_api_UvicornServer(mocker): thread_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.threading.Thread') s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) assert thread_mock.call_count == 0 @@ -248,6 +248,38 @@ def test_api_UvicornServer(default_conf, mocker): assert s.should_exit is True +def test_api_UvicornServer_run(mocker): + serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve', + get_mock_coro(None)) + s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) + assert serve_mock.call_count == 0 + + s.install_signal_handlers() + # Original implementation starts a thread - make sure that's not the case + assert serve_mock.call_count == 0 + + # Fake started to avoid sleeping forever + s.started = True + s.run() + assert serve_mock.call_count == 1 + + +def test_api_UvicornServer_run_no_uvloop(mocker, import_fails): + serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve', + get_mock_coro(None)) + s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) + assert serve_mock.call_count == 0 + + s.install_signal_handlers() + # Original implementation starts a thread - make sure that's not the case + assert serve_mock.call_count == 0 + + # Fake started to avoid sleeping forever + s.started = True + s.run() + assert serve_mock.call_count == 1 + + def test_api_run(default_conf, mocker, caplog): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", From eb4f05eb23efa08be363a374054439b0a3ccd07e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Mar 2021 16:30:47 +0100 Subject: [PATCH 517/563] Add documentation for hyperopt.strategy availability --- docs/advanced-hyperopt.md | 47 ++++++++++++++++++++++++++++++++++++++- docs/hyperopt.md | 2 +- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index d2237b3e8..bdaafb936 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -6,7 +6,7 @@ class. ## Derived hyperopt classes -Custom hyperop classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies). +Custom hyperopt classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies). Applying to hyperoptimization, as an example, you may override how dimensions are defined in your optimization hyperspace: @@ -32,6 +32,51 @@ or $ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... ``` +## Sharing methods with your strategy + +Hyperopt classes provide access to the Strategy via the `strategy` class attribute. +This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users. + +``` python +from pandas import DataFrame +from freqtrade.strategy.interface import IStrategy +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class MyAwesomeStrategy(IStrategy): + + buy_params = { + 'rsi-value': 30, + 'adx-value': 35, + } + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + return self.buy_strategy_generator(self.buy_params, dataframe, metadata) + + @staticmethod + def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) & + dataframe['adx'] > params['adx-value']) & + dataframe['volume'] > 0 + ) + , 'buy'] = 1 + return dataframe + +class MyAwesomeHyperOpt(IHyperOpt): + ... + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + # Call strategy's buy strategy generator + return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata) + + return populate_buy_trend +``` + ## Creating and using a custom loss function To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. diff --git a/docs/hyperopt.md b/docs/hyperopt.md index d6959b457..69bc57d1a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -283,7 +283,7 @@ So let's write the buy strategy using these values: """ Define the buy strategy parameters to be used by Hyperopt. """ - def populate_buy_trend(dataframe: DataFrame) -> DataFrame: + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # GUARDS AND TRENDS if 'adx-enabled' in params and params['adx-enabled']: From 618bae23a64930c3b4cb84fb1298e8a0e7b4c40d Mon Sep 17 00:00:00 2001 From: Jackson Law <178053+jlaw@users.noreply.github.com> Date: Sat, 13 Mar 2021 11:14:36 -0800 Subject: [PATCH 518/563] fix: Use now() to match timezone of download data --- tests/commands/test_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index d5e76eeb6..27875ac94 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -706,7 +706,7 @@ def test_download_data_timerange(mocker, caplog, markets): start_download_data(get_args(args)) assert dl_mock.call_count == 1 # 20days ago - days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).int_timestamp + days_ago = arrow.get(arrow.now().shift(days=-20).date()).int_timestamp assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago dl_mock.reset_mock() From b57c150654235086164f1199584ce990aa38f854 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Mar 2021 09:48:40 +0100 Subject: [PATCH 519/563] Final balance should include forcesold pairs --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f2cf0d0dc..0b884dae5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -301,6 +301,7 @@ class Backtesting: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = SellType.FORCE_SELL.value trade.close(sell_row[OPEN_IDX], show_msg=False) + LocalTrade.close_bt_trade(trade) # Deepcopy object to have wallets update correctly trade1 = deepcopy(trade) trade1.is_open = True From 7a63f8cc319d9543b9041caa5be9497e6fb01d4d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Mar 2021 13:25:08 +0100 Subject: [PATCH 520/563] Fix hdf5 support on raspberry --- Dockerfile.armhf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index f938ec457..eecd9fdc0 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -41,7 +41,9 @@ COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir \ +RUN apt-get install -y libhdf5-serial-dev \ + && apt-get clean \ + && pip install -e . --no-cache-dir \ && freqtrade install-ui ENTRYPOINT ["freqtrade"] From e92441643157652467eed377a43d057d32c18708 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Sun, 14 Mar 2021 22:02:53 +0900 Subject: [PATCH 521/563] correct math used in examples and clarify some terminology regarding custom stoploss functions --- docs/strategy-advanced.md | 64 +++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 56061365e..cda988acd 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -71,12 +71,13 @@ See `custom_stoploss` examples below on how to access the saved dataframe column ## Custom stoploss -A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. -Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. +The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object. -The method must return a stoploss value (float / number) with a relative ratio below the current price. -E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`). +The method must return a stoploss value (float / number) as a percentage of the current price. +E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD. + +The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: @@ -177,16 +178,33 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` + +#### Calculating stoploss relative to open price + +Stoploss values returned from `custom_stoploss` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +This can be calculated as: + +``` python +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + return 1-((1+open_relative_stop)/(1+current_profit)) + +``` + +For example, say our open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, 0.21)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + #### Trailing stoploss with positive offset Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. -Please note that the stoploss can only increase, values lower than the current stoploss are ignored. ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + return 1-((1+open_relative_stop)/(1+current_profit)) + class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -197,28 +215,32 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: if current_profit < 0.04: - return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss + return 1 # return a value bigger than the inital stoploss to keep using the inital stoploss # After reaching the desired offset, allow the stoploss to trail by half the profit - desired_stoploss = current_profit / 2 - # Use a minimum of 2.5% and a maximum of 5% - return max(min(desired_stoploss, 0.05), 0.025) + desired_stop_from_open = max(min(current_profit / 2, 0.05), 0.025) + + return stoploss_from_open(desired_stop_from_open, current_profit) ``` -#### Absolute stoploss +#### Stepped stoploss -The below example sets absolute profit levels based on the current profit. +Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. * Use the regular stoploss until 20% profit is reached -* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. -* Once profit is > 25% - stoploss will be 15%. -* Once profit is > 20% - stoploss will be set to 7%. +* Once profit is > 20% - set stoploss to 7% above open price. +* Once profit is > 25% - set stoploss to 15% above open price. +* Once profit is > 40% - set stoploss to 25% above open price. + ``` python from datetime import datetime from freqtrade.persistence import Trade +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + return 1-((1+open_relative_stop)/(1+current_profit)) + class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -228,13 +250,15 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price + # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: - return (-0.25 + current_profit) - if current_profit > 0.25: - return (-0.15 + current_profit) - if current_profit > 0.20: - return (-0.07 + current_profit) + return stoploss_from_open(0.25, current_profit) + elif current_profit > 0.25: + return stoploss_from_open(0.15, current_profit) + elif current_profit > 0.20: + return stoploss_from_open(0.07, current_profit) + + # return maximum stoploss value, keeping current stoploss price unchanged return 1 ``` #### Custom stoploss using an indicator from dataframe example From 0b35c0571fd1d0ac5beadd645c2c87958331e61a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Mar 2021 19:37:30 +0100 Subject: [PATCH 522/563] Allow custom fee to be used during dry-run closes #3696 --- docs/configuration.md | 1 + freqtrade/commands/arguments.py | 2 +- freqtrade/exchange/exchange.py | 2 ++ tests/exchange/test_exchange.py | 8 ++++++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 20b26ec13..2e8edca2e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -58,6 +58,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Float | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
**Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean +| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 88cec7b3e..9468a7f7d 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -14,7 +14,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", ] +ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", "max_open_trades", "stake_amount", "fee"] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 138be3537..fdb34eb41 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1232,6 +1232,8 @@ class Exchange: def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1, price: float = 1, taker_or_maker: str = 'maker') -> float: try: + if self._config['dry_run'] and self._config.get('fee', None) is not None: + return self._config['fee'] # validate that markets are loaded before trying to get fee if self._api.markets is None or len(self._api.markets) == 0: self._api.load_markets() diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3fd566fa3..8a8c95a62 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2268,12 +2268,20 @@ def test_get_fee(default_conf, mocker, exchange_name): 'cost': 0.05 }) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange._config.pop('fee', None) assert exchange.get_fee('ETH/BTC') == 0.025 + assert api_mock.calculate_fee.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_fee', 'calculate_fee', symbol="ETH/BTC") + api_mock.calculate_fee.reset_mock() + exchange._config['fee'] = 0.001 + + assert exchange.get_fee('ETH/BTC') == 0.001 + assert api_mock.calculate_fee.call_count == 0 + def test_stoploss_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='bittrex') From b191663a7ec512c68607a40408ae9e07f0d5275d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Mar 2021 19:49:46 +0100 Subject: [PATCH 523/563] Adapt hyperopt templates to be better aligned closes #3027 --- freqtrade/templates/base_hyperopt.py.j2 | 23 ++--- freqtrade/templates/sample_hyperopt.py | 94 ++++++------------ .../templates/sample_hyperopt_advanced.py | 97 ++++++------------- 3 files changed, 72 insertions(+), 142 deletions(-) diff --git a/freqtrade/templates/base_hyperopt.py.j2 b/freqtrade/templates/base_hyperopt.py.j2 index ec787cbb6..f6ca1477a 100644 --- a/freqtrade/templates/base_hyperopt.py.j2 +++ b/freqtrade/templates/base_hyperopt.py.j2 @@ -39,6 +39,15 @@ class {{ hyperopt }}(IHyperOpt): https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py. """ + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching buy strategy parameters. + """ + return [ + {{ buy_space | indent(12) }} + ] + @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ @@ -79,12 +88,12 @@ class {{ hyperopt }}(IHyperOpt): return populate_buy_trend @staticmethod - def indicator_space() -> List[Dimension]: + def sell_indicator_space() -> List[Dimension]: """ - Define your Hyperopt space for searching buy strategy parameters. + Define your Hyperopt space for searching sell strategy parameters. """ return [ - {{ buy_space | indent(12) }} + {{ sell_space | indent(12) }} ] @staticmethod @@ -126,11 +135,3 @@ class {{ hyperopt }}(IHyperOpt): return populate_sell_trend - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters. - """ - return [ - {{ sell_space | indent(12) }} - ] diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py index 10743e911..ed1af7718 100644 --- a/freqtrade/templates/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -45,6 +45,23 @@ class SampleHyperOpt(IHyperOpt): https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py. """ + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching buy strategy parameters. + """ + return [ + Integer(10, 25, name='mfi-value'), + Integer(15, 45, name='fastd-value'), + Integer(20, 50, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='mfi-enabled'), + Categorical([True, False], name='fastd-enabled'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + ] + @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ @@ -92,20 +109,22 @@ class SampleHyperOpt(IHyperOpt): return populate_buy_trend @staticmethod - def indicator_space() -> List[Dimension]: + def sell_indicator_space() -> List[Dimension]: """ - Define your Hyperopt space for searching buy strategy parameters. + Define your Hyperopt space for searching sell strategy parameters. """ return [ - Integer(10, 25, name='mfi-value'), - Integer(15, 45, name='fastd-value'), - Integer(20, 50, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='mfi-enabled'), - Categorical([True, False], name='fastd-enabled'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + Integer(75, 100, name='sell-mfi-value'), + Integer(50, 100, name='sell-fastd-value'), + Integer(50, 100, name='sell-adx-value'), + Integer(60, 100, name='sell-rsi-value'), + Categorical([True, False], name='sell-mfi-enabled'), + Categorical([True, False], name='sell-fastd-enabled'), + Categorical([True, False], name='sell-adx-enabled'), + Categorical([True, False], name='sell-rsi-enabled'), + Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') ] @staticmethod @@ -153,56 +172,3 @@ class SampleHyperOpt(IHyperOpt): return dataframe return populate_sell_trend - - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters. - """ - return [ - Integer(75, 100, name='sell-mfi-value'), - Integer(50, 100, name='sell-fastd-value'), - Integer(50, 100, name='sell-adx-value'), - Integer(60, 100, name='sell-rsi-value'), - Categorical([True, False], name='sell-mfi-enabled'), - Categorical([True, False], name='sell-fastd-enabled'), - Categorical([True, False], name='sell-adx-enabled'), - Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-bb_upper', - 'sell-macd_cross_signal', - 'sell-sar_reversal'], name='sell-trigger') - ] - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. Should be a copy of same method from strategy. - Must align to populate_indicators in this file. - Only used when --spaces does not include buy space. - """ - dataframe.loc[ - ( - (dataframe['close'] < dataframe['bb_lowerband']) & - (dataframe['mfi'] < 16) & - (dataframe['adx'] > 25) & - (dataframe['rsi'] < 21) - ), - 'buy'] = 1 - - return dataframe - - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. Should be a copy of same method from strategy. - Must align to populate_indicators in this file. - Only used when --spaces does not include sell space. - """ - dataframe.loc[ - ( - (qtpylib.crossed_above( - dataframe['macdsignal'], dataframe['macd'] - )) & - (dataframe['fastd'] > 54) - ), - 'sell'] = 1 - - return dataframe diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 52e397466..7736570f7 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -60,6 +60,23 @@ class AdvancedSampleHyperOpt(IHyperOpt): dataframe['sar'] = ta.SAR(dataframe) return dataframe + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching buy strategy parameters. + """ + return [ + Integer(10, 25, name='mfi-value'), + Integer(15, 45, name='fastd-value'), + Integer(20, 50, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='mfi-enabled'), + Categorical([True, False], name='fastd-enabled'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + ] + @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ @@ -106,20 +123,22 @@ class AdvancedSampleHyperOpt(IHyperOpt): return populate_buy_trend @staticmethod - def indicator_space() -> List[Dimension]: + def sell_indicator_space() -> List[Dimension]: """ - Define your Hyperopt space for searching strategy parameters + Define your Hyperopt space for searching sell strategy parameters. """ return [ - Integer(10, 25, name='mfi-value'), - Integer(15, 45, name='fastd-value'), - Integer(20, 50, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='mfi-enabled'), - Categorical([True, False], name='fastd-enabled'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + Integer(75, 100, name='sell-mfi-value'), + Integer(50, 100, name='sell-fastd-value'), + Integer(50, 100, name='sell-adx-value'), + Integer(60, 100, name='sell-rsi-value'), + Categorical([True, False], name='sell-mfi-enabled'), + Categorical([True, False], name='sell-fastd-enabled'), + Categorical([True, False], name='sell-adx-enabled'), + Categorical([True, False], name='sell-rsi-enabled'), + Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') ] @staticmethod @@ -168,25 +187,6 @@ class AdvancedSampleHyperOpt(IHyperOpt): return populate_sell_trend - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters - """ - return [ - Integer(75, 100, name='sell-mfi-value'), - Integer(50, 100, name='sell-fastd-value'), - Integer(50, 100, name='sell-adx-value'), - Integer(60, 100, name='sell-rsi-value'), - Categorical([True, False], name='sell-mfi-enabled'), - Categorical([True, False], name='sell-fastd-enabled'), - Categorical([True, False], name='sell-adx-enabled'), - Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-bb_upper', - 'sell-macd_cross_signal', - 'sell-sar_reversal'], name='sell-trigger') - ] - @staticmethod def generate_roi_table(params: Dict) -> Dict[int, float]: """ @@ -267,40 +267,3 @@ class AdvancedSampleHyperOpt(IHyperOpt): Categorical([True, False], name='trailing_only_offset_is_reached'), ] - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. - Can be a copy of the corresponding method from the strategy, - or will be loaded from the strategy. - Must align to populate_indicators used (either from this File, or from the strategy) - Only used when --spaces does not include buy - """ - dataframe.loc[ - ( - (dataframe['close'] < dataframe['bb_lowerband']) & - (dataframe['mfi'] < 16) & - (dataframe['adx'] > 25) & - (dataframe['rsi'] < 21) - ), - 'buy'] = 1 - - return dataframe - - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. - Can be a copy of the corresponding method from the strategy, - or will be loaded from the strategy. - Must align to populate_indicators used (either from this File, or from the strategy) - Only used when --spaces does not include sell - """ - dataframe.loc[ - ( - (qtpylib.crossed_above( - dataframe['macdsignal'], dataframe['macd'] - )) & - (dataframe['fastd'] > 54) - ), - 'sell'] = 1 - return dataframe From 09872d8e42cd6a2ccccb228bad037c0aa90def42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 05:27:18 +0000 Subject: [PATCH 524/563] Bump flake8 from 3.8.4 to 3.9.0 Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.8.4 to 3.9.0. - [Release notes](https://gitlab.com/pycqa/flake8/tags) - [Commits](https://gitlab.com/pycqa/flake8/compare/3.8.4...3.9.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 68b1dd53f..4f0ea7706 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements-hyperopt.txt coveralls==3.0.1 -flake8==3.8.4 +flake8==3.9.0 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 From 22c34faca388ee38887813710825e2283c7651ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 05:28:02 +0000 Subject: [PATCH 525/563] Bump mkdocs-material from 7.0.5 to 7.0.6 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.0.5 to 7.0.6. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.0.5...7.0.6) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 22c09ff69..0068dd5d2 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.0.5 +mkdocs-material==7.0.6 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 1173d8971a57f4005a48b4067110fd985d90e726 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 05:28:06 +0000 Subject: [PATCH 526/563] Bump prompt-toolkit from 3.0.16 to 3.0.17 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.16 to 3.0.17. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f62c8ff52..1cbfadb17 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,4 +39,4 @@ aiofiles==0.6.0 colorama==0.4.4 # Building config files interactively questionary==1.9.0 -prompt-toolkit==3.0.16 +prompt-toolkit==3.0.17 From a209b0a392415c3b2b8382f097f4982c10497fe7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 05:28:18 +0000 Subject: [PATCH 527/563] Bump python-telegram-bot from 13.3 to 13.4.1 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.3 to 13.4.1. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.3...v13.4.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f62c8ff52..77d22dbf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==1.42.66 cryptography==3.4.6 aiohttp==3.7.4.post0 SQLAlchemy==1.3.23 -python-telegram-bot==13.3 +python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 requests==2.25.1 From b6c29bebb07bbab854f1086c75df7b8ad16d6ab3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Mar 2021 06:56:48 +0100 Subject: [PATCH 528/563] Update slack action --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f294347a..61ecaa522 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,7 +102,7 @@ jobs: mypy freqtrade scripts - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} @@ -194,7 +194,7 @@ jobs: mypy freqtrade scripts - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} @@ -257,7 +257,7 @@ jobs: mypy freqtrade scripts - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} @@ -288,7 +288,7 @@ jobs: mkdocs build - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} @@ -311,7 +311,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} @@ -398,7 +398,7 @@ jobs: - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} From 8f26935259f20fadf33d3b928fa5f6a1dde10a43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 09:26:02 +0000 Subject: [PATCH 529/563] Bump ccxt from 1.42.66 to 1.43.27 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.42.66 to 1.43.27. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.42.66...1.43.27) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 77d22dbf3..090cdc588 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.3 -ccxt==1.42.66 +ccxt==1.43.27 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 From 79d4585dadf14e7e3749cabb498ef8cfe47f99eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Mar 2021 19:24:03 +0100 Subject: [PATCH 530/563] Add check to ensure close_profit_abs is filled on closed trades Technically, this should not be possible, but #4554 shows it is. closes #4554 --- freqtrade/wallets.py | 3 ++- tests/test_persistence.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 575fe1b67..f4432e932 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -71,7 +71,8 @@ class Wallets: # TODO: potentially remove the ._log workaround to determine backtest mode. if self._log: closed_trades = Trade.get_trades_proxy(is_open=False) - tot_profit = sum([trade.close_profit_abs for trade in closed_trades]) + tot_profit = sum( + [trade.close_profit_abs for trade in closed_trades if trade.close_profit_abs]) else: tot_profit = LocalTrade.total_profit tot_in_trades = sum([trade.stake_amount for trade in open_trades]) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index ab900cbb8..1820250a5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1066,8 +1066,14 @@ def test_get_trades_proxy(fee, use_db): assert isinstance(trades[0], Trade) - assert len(Trade.get_trades_proxy(is_open=True)) == 4 - assert len(Trade.get_trades_proxy(is_open=False)) == 2 + trades = Trade.get_trades_proxy(is_open=True) + assert len(trades) == 4 + assert trades[0].is_open + trades = Trade.get_trades_proxy(is_open=False) + + assert len(trades) == 2 + assert not trades[0].is_open + opendate = datetime.now(tz=timezone.utc) - timedelta(minutes=15) assert len(Trade.get_trades_proxy(open_date=opendate)) == 3 From aee2591490b442a731c8efb039658e8230958cd5 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Wed, 17 Mar 2021 17:58:23 +0900 Subject: [PATCH 531/563] add stoploss_from_open() as a strategy_helper --- docs/strategy-customization.md | 36 +++++++++++++++++++++++++++ freqtrade/strategy/__init__.py | 1 + freqtrade/strategy/strategy_helper.py | 18 ++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a1708a481..bf086bc0a 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -587,6 +587,42 @@ All columns of the informative dataframe will be available on the returning data *** +### *stoploss_from_open()* + +Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the open price instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired percentage above the open price. + +??? Example "Returning a stoploss relative to the open price from the custom stoploss function" + + Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). + + If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + + + ``` python + + from freqtrade.strategy import IStrategy, stoploss_from_open + from datetime import datetime + from freqtrade.persistence import Trade + + class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + # once the profit has risin above 10%, keep the stoploss at 7% above the open price + if current_profit > 0.10: + return stoploss_from_open(0.07, current_profit) + + return 1 + + ``` + + + ## Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 662156ae9..3de90666e 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,3 +3,4 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_helper import merge_informative_pair +from freqtrade.strategy.strategy_helper import stoploss_from_open diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index d7b1327d9..f40fa285d 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -56,3 +56,21 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, dataframe = dataframe.ffill() return dataframe + + +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + """ + + Given the current profit, and a desired stop loss value relative to the open price, + return a stop loss value that is relative to the current price, and which can be + returned from `custom_stoploss`. + + :param open_relative_stop: Desired stop loss value relative to open price + :param current_profit: The current profit percentage + :return: Stop loss value relative to current price + """ + + if current_profit == -1: + return 1 + + return 1-((1+open_relative_stop)/(1+current_profit)) From ce1ed76269370862f6f717c4d9ffe98b049d7caa Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Wed, 17 Mar 2021 22:44:10 +0900 Subject: [PATCH 532/563] complete stoploss_from_open and associated test --- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/strategy_helper.py | 15 ++++++++-- tests/strategy/test_strategy_helpers.py | 39 ++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 3de90666e..85148b6ea 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,5 +2,4 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import merge_informative_pair -from freqtrade.strategy.strategy_helper import stoploss_from_open +from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index f40fa285d..22b6f0be5 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -65,12 +65,21 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa return a stop loss value that is relative to the current price, and which can be returned from `custom_stoploss`. - :param open_relative_stop: Desired stop loss value relative to open price + The requested stop can be positive for a stop above the open price, or negative for + a stop below the open price. The return value is always >= 0. + + Returns 0 if the resulting stop price would be above the current price. + + :param open_relative_stop: Desired stop loss percentage relative to open price :param current_profit: The current profit percentage - :return: Stop loss value relative to current price + :return: Positive stop loss value relative to current price """ + # formula is undefined for current_profit -1, return maximum value if current_profit == -1: return 1 - return 1-((1+open_relative_stop)/(1+current_profit)) + stoploss = 1-((1+open_relative_stop)/(1+current_profit)) + + # negative stoploss values indicate the requested stop price is higher than the current price + return max(stoploss, 0.0) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 252288e2e..3b84fc254 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -1,8 +1,10 @@ +from math import isclose + import numpy as np import pandas as pd import pytest -from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes +from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes def generate_test_data(timeframe: str, size: int): @@ -95,3 +97,38 @@ def test_merge_informative_pair_lower(): with pytest.raises(ValueError, match=r"Tried to merge a faster timeframe .*"): merge_informative_pair(data, informative, '1h', '15m', ffill=True) + + +def test_stoploss_from_open(): + open_price_ranges = [ + [0.01, 1.00, 30], + [1, 100, 30], + [100, 10000, 30], + ] + current_profit_range = [-0.99, 2, 30] + desired_stop_range = [-0.50, 0.50, 30] + + for open_range in open_price_ranges: + for open_price in np.linspace(*open_range): + for desired_stop in np.linspace(*desired_stop_range): + + # -1 is not a valid current_profit, should return 1 + assert stoploss_from_open(desired_stop, -1) == 1 + + for current_profit in np.linspace(*current_profit_range): + current_price = open_price * (1 + current_profit) + expected_stop_price = open_price * (1 + desired_stop) + + stoploss = stoploss_from_open(desired_stop, current_profit) + + assert stoploss >= 0 + assert stoploss <= 1 + + stop_price = current_price * (1 - stoploss) + + # there is no correct answer if the expected stop price is above + # the current price + if expected_stop_price > current_price: + assert stoploss == 0 + else: + assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) From 6597055a24f5592057f62ec330e72dda310a606c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Mar 2021 19:36:11 +0100 Subject: [PATCH 533/563] Ensure ccxt tests run without dry-run closes #4566 --- tests/exchange/test_ccxt_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 03cb30d62..870e6cabd 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -44,6 +44,7 @@ EXCHANGES = { def exchange_conf(): config = get_default_conf((Path(__file__).parent / "testdata").resolve()) config['exchange']['pair_whitelist'] = [] + config['dry_run'] = False return config From b05de6d4687299f5012aec4eeebc0ed8dbebb173 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Mar 2021 19:36:35 +0100 Subject: [PATCH 534/563] Move advanced exchange config to exchange page --- docs/configuration.md | 20 -------------------- docs/exchanges.md | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 2e8edca2e..ca1e03b0a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -417,26 +417,6 @@ This configuration enables binance, as well as rate limiting to avoid bans from Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step. -#### Advanced Freqtrade Exchange configuration - -Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behaviours. - -Available options are listed in the exchange-class as `_ft_has_default`. - -For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call): - -```json -"exchange": { - "name": "kraken", - "_ft_has_params": { - "order_time_in_force": ["gtc", "fok"], - "ohlcv_candle_limit": 200 - } -``` - -!!! Warning - Please make sure to fully understand the impacts of these settings before modifying them. - ### What values can be used for fiat_display_currency? The `fiat_display_currency` configuration parameter sets the base currency to use for the diff --git a/docs/exchanges.md b/docs/exchanges.md index 2e5bdfadd..4c7e44b06 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -118,3 +118,23 @@ Whether your exchange returns incomplete candles or not can be checked using [th Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle. However, if it is based on the need for the latest price for your strategy - then this requirement can be acquired using the [data provider](strategy-customization.md#possible-options-for-dataprovider) from within the strategy. + +### Advanced Freqtrade Exchange configuration + +Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behavior. + +Available options are listed in the exchange-class as `_ft_has_default`. + +For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call): + +```json +"exchange": { + "name": "kraken", + "_ft_has_params": { + "order_time_in_force": ["gtc", "fok"], + "ohlcv_candle_limit": 200 + } +``` + +!!! Warning + Please make sure to fully understand the impacts of these settings before modifying them. From 76ca3c219f9d7b7ccf8a2723267529acbf54f658 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Mar 2021 20:43:51 +0100 Subject: [PATCH 535/563] extract result-printing from hyperopt class --- freqtrade/commands/hyperopt_commands.py | 21 +- freqtrade/optimize/hyperopt.py | 290 +---------------------- freqtrade/optimize/hyperopt_tools.py | 294 ++++++++++++++++++++++++ tests/commands/test_commands.py | 4 +- tests/optimize/test_hyperopt.py | 19 +- 5 files changed, 324 insertions(+), 304 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_tools.py diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index fd8f737f0..268e3eeef 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -17,7 +17,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: """ List hyperopt epochs previously evaluated """ - from freqtrade.optimize.hyperopt import Hyperopt + from freqtrade.optimize.hyperopt_tools import HyperoptTools config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -47,7 +47,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: config.get('hyperoptexportfilename')) # Previous evaluations - epochs = Hyperopt.load_previous_results(results_file) + epochs = HyperoptTools.load_previous_results(results_file) total_epochs = len(epochs) epochs = hyperopt_filter_epochs(epochs, filteroptions) @@ -57,18 +57,19 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if not export_csv: try: - print(Hyperopt.get_result_table(config, epochs, total_epochs, - not filteroptions['only_best'], print_colorized, 0)) + print(HyperoptTools.get_result_table(config, epochs, total_epochs, + not filteroptions['only_best'], + print_colorized, 0)) except KeyboardInterrupt: print('User interrupted..') if epochs and not no_details: sorted_epochs = sorted(epochs, key=itemgetter('loss')) results = sorted_epochs[0] - Hyperopt.print_epoch_details(results, total_epochs, print_json, no_header) + HyperoptTools.print_epoch_details(results, total_epochs, print_json, no_header) if epochs and export_csv: - Hyperopt.export_csv_file( + HyperoptTools.export_csv_file( config, epochs, total_epochs, not filteroptions['only_best'], export_csv ) @@ -77,7 +78,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: """ Show details of a hyperopt epoch previously evaluated """ - from freqtrade.optimize.hyperopt import Hyperopt + from freqtrade.optimize.hyperopt_tools import HyperoptTools config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -105,7 +106,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: } # Previous evaluations - epochs = Hyperopt.load_previous_results(results_file) + epochs = HyperoptTools.load_previous_results(results_file) total_epochs = len(epochs) epochs = hyperopt_filter_epochs(epochs, filteroptions) @@ -124,8 +125,8 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: if epochs: val = epochs[n] - Hyperopt.print_epoch_details(val, total_epochs, print_json, no_header, - header_str="Epoch details") + HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header, + header_str="Epoch details") def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 6b5bc171b..03f34a511 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,36 +4,31 @@ This module contains the hyperopt logic """ -import io import locale import logging import random import warnings -from collections import OrderedDict from datetime import datetime from math import ceil from operator import itemgetter from pathlib import Path -from pprint import pformat from typing import Any, Dict, List, Optional import progressbar -import rapidjson -import tabulate from colorama import Fore, Style from colorama import init as colorama_init from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects -from pandas import DataFrame, isna, json_normalize +from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange -from freqtrade.exceptions import OperationalException -from freqtrade.misc import file_dump_json, plural, round_dict +from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 +from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy @@ -169,15 +164,6 @@ class Hyperopt: file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)}, log=False) - @staticmethod - def _read_results(results_file: Path) -> List: - """ - Read hyperopt results from file - """ - logger.info("Reading epochs from '%s'", results_file) - data = load(results_file) - return data - def _get_params_details(self, params: Dict) -> Dict: """ Return the params for each space @@ -200,102 +186,16 @@ class Hyperopt: return result - @staticmethod - def print_epoch_details(results, total_epochs: int, print_json: bool, - no_header: bool = False, header_str: str = None) -> None: - """ - Display details of the hyperopt result - """ - params = results.get('params_details', {}) - - # Default header string - if header_str is None: - header_str = "Best result" - - if not no_header: - explanation_str = Hyperopt._format_explanation_string(results, total_epochs) - print(f"\n{header_str}:\n\n{explanation_str}\n") - - if print_json: - result_dict: Dict = {} - for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: - Hyperopt._params_update_for_json(result_dict, params, s) - print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) - - else: - Hyperopt._params_pretty_print(params, 'buy', "Buy hyperspace params:") - Hyperopt._params_pretty_print(params, 'sell', "Sell hyperspace params:") - Hyperopt._params_pretty_print(params, 'roi', "ROI table:") - Hyperopt._params_pretty_print(params, 'stoploss', "Stoploss:") - Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:") - - @staticmethod - def _params_update_for_json(result_dict, params, space: str) -> None: - if space in params: - space_params = Hyperopt._space_params(params, space) - if space in ['buy', 'sell']: - result_dict.setdefault('params', {}).update(space_params) - elif space == 'roi': - # TODO: get rid of OrderedDict when support for python 3.6 will be - # dropped (dicts keep the order as the language feature) - - # Convert keys in min_roi dict to strings because - # rapidjson cannot dump dicts with integer keys... - # OrderedDict is used to keep the numeric order of the items - # in the dict. - result_dict['minimal_roi'] = OrderedDict( - (str(k), v) for k, v in space_params.items() - ) - else: # 'stoploss', 'trailing' - result_dict.update(space_params) - - @staticmethod - def _params_pretty_print(params, space: str, header: str) -> None: - if space in params: - space_params = Hyperopt._space_params(params, space, 5) - params_result = f"\n# {header}\n" - if space == 'stoploss': - params_result += f"stoploss = {space_params.get('stoploss')}" - elif space == 'roi': - # TODO: get rid of OrderedDict when support for python 3.6 will be - # dropped (dicts keep the order as the language feature) - minimal_roi_result = rapidjson.dumps( - OrderedDict( - (str(k), v) for k, v in space_params.items() - ), - default=str, indent=4, number_mode=rapidjson.NM_NATIVE) - params_result += f"minimal_roi = {minimal_roi_result}" - elif space == 'trailing': - - for k, v in space_params.items(): - params_result += f'{k} = {v}\n' - - else: - params_result += f"{space}_params = {pformat(space_params, indent=4)}" - params_result = params_result.replace("}", "\n}").replace("{", "{\n ") - - params_result = params_result.replace("\n", "\n ") - print(params_result) - - @staticmethod - def _space_params(params, space: str, r: int = None) -> Dict: - d = params[space] - # Round floats to `r` digits after the decimal point if requested - return round_dict(d, r) if r else d - - @staticmethod - def is_best_loss(results, current_best_loss: float) -> bool: - return results['loss'] < current_best_loss - def print_results(self, results) -> None: """ Log results if it is better than any previous evaluation + TODO: this should be moved to HyperoptTools too """ is_best = results['is_best'] if self.print_all or is_best: print( - self.get_result_table( + HyperoptTools.get_result_table( self.config, results, self.total_epochs, self.print_all, self.print_colorized, self.hyperopt_table_header @@ -303,166 +203,6 @@ class Hyperopt: ) self.hyperopt_table_header = 2 - @staticmethod - def _format_explanation_string(results, total_epochs) -> str: - return (("*" if results['is_initial_point'] else " ") + - f"{results['current_epoch']:5d}/{total_epochs}: " + - f"{results['results_explanation']} " + - f"Objective: {results['loss']:.5f}") - - @staticmethod - def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool, - print_colorized: bool, remove_header: int) -> str: - """ - Log result table - """ - if not results: - return '' - - tabulate.PRESERVE_WHITESPACE = True - - trials = json_normalize(results, max_level=1) - trials['Best'] = '' - if 'results_metrics.winsdrawslosses' not in trials.columns: - # Ensure compatibility with older versions of hyperopt results - trials['results_metrics.winsdrawslosses'] = 'N/A' - - trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.winsdrawslosses', - 'results_metrics.avg_profit', 'results_metrics.total_profit', - 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best']] - trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', - 'Total profit', 'Profit', 'Avg duration', 'Objective', - 'is_initial_point', 'is_best'] - trials['is_profit'] = False - trials.loc[trials['is_initial_point'], 'Best'] = '* ' - trials.loc[trials['is_best'], 'Best'] = 'Best' - trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' - trials.loc[trials['Total profit'] > 0, 'is_profit'] = True - trials['Trades'] = trials['Trades'].astype(str) - - trials['Epoch'] = trials['Epoch'].apply( - lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) - ) - trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') - ) - trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') - ) - trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') - ) - - trials['Profit'] = trials.apply( - lambda x: '{:,.8f} {} {}'.format( - x['Total profit'], config['stake_currency'], - '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') - ).rjust(25+len(config['stake_currency'])) - if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), - axis=1 - ) - trials = trials.drop(columns=['Total profit']) - - if print_colorized: - for i in range(len(trials)): - if trials.loc[i]['is_profit']: - for j in range(len(trials.loc[i])-3): - trials.iat[i, j] = "{}{}{}".format(Fore.GREEN, - str(trials.loc[i][j]), Fore.RESET) - if trials.loc[i]['is_best'] and highlight_best: - for j in range(len(trials.loc[i])-3): - trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT, - str(trials.loc[i][j]), Style.RESET_ALL) - - trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) - if remove_header > 0: - table = tabulate.tabulate( - trials.to_dict(orient='list'), tablefmt='orgtbl', - headers='keys', stralign="right" - ) - - table = table.split("\n", remove_header)[remove_header] - elif remove_header < 0: - table = tabulate.tabulate( - trials.to_dict(orient='list'), tablefmt='psql', - headers='keys', stralign="right" - ) - table = "\n".join(table.split("\n")[0:remove_header]) - else: - table = tabulate.tabulate( - trials.to_dict(orient='list'), tablefmt='psql', - headers='keys', stralign="right" - ) - return table - - @staticmethod - def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, - csv_file: str) -> None: - """ - Log result to csv-file - """ - if not results: - return - - # Verification for overwrite - if Path(csv_file).is_file(): - logger.error(f"CSV file already exists: {csv_file}") - return - - try: - io.open(csv_file, 'w+').close() - except IOError: - logger.error(f"Failed to create CSV file: {csv_file}") - return - - trials = json_normalize(results, max_level=1) - trials['Best'] = '' - trials['Stake currency'] = config['stake_currency'] - - base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.median_profit', - 'results_metrics.total_profit', - 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best'] - param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] - trials = trials[base_metrics + param_metrics] - - base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', - 'Stake currency', 'Profit', 'Avg duration', 'Objective', - 'is_initial_point', 'is_best'] - param_columns = list(results[0]['params_dict'].keys()) - trials.columns = base_columns + param_columns - - trials['is_profit'] = False - trials.loc[trials['is_initial_point'], 'Best'] = '*' - trials.loc[trials['is_best'], 'Best'] = 'Best' - trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' - trials.loc[trials['Total profit'] > 0, 'is_profit'] = True - trials['Epoch'] = trials['Epoch'].astype(str) - trials['Trades'] = trials['Trades'].astype(str) - - trials['Total profit'] = trials['Total profit'].apply( - lambda x: '{:,.8f}'.format(x) if x != 0.0 else "" - ) - trials['Profit'] = trials['Profit'].apply( - lambda x: '{:,.2f}'.format(x) if not isna(x) else "" - ) - trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x) if not isna(x) else "" - ) - trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x) if not isna(x) else "" - ) - trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x) if x != 100000 else "" - ) - - trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) - trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8') - logger.info(f"CSV file created: {csv_file}") - def has_space(self, space: str) -> bool: """ Tell if the space value is contained in the configuration @@ -626,22 +366,6 @@ class Hyperopt: return parallel(delayed( wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked) - @staticmethod - def load_previous_results(results_file: Path) -> List: - """ - Load data for epochs from the file if we have one - """ - epochs: List = [] - if results_file.is_file() and results_file.stat().st_size > 0: - epochs = Hyperopt._read_results(results_file) - # Detection of some old format, without 'is_best' field saved - if epochs[0].get('is_best') is None: - raise OperationalException( - "The file with Hyperopt results is incompatible with this version " - "of Freqtrade and cannot be loaded.") - logger.info(f"Loaded {len(epochs)} previous evaluations from disk.") - return epochs - def _set_random_state(self, random_state: Optional[int]) -> int: return random_state or random.randint(1, 2**16 - 1) @@ -734,7 +458,7 @@ class Hyperopt: logger.debug(f"Optimizer epoch evaluated: {val}") - is_best = self.is_best_loss(val, self.current_best_loss) + is_best = HyperoptTools.is_best_loss(val, self.current_best_loss) # This value is assigned here and not in the optimization method # to keep proper order in the list of results. That's because # evaluations can take different time. Here they are aligned in the @@ -762,7 +486,7 @@ class Hyperopt: if self.epochs: sorted_epochs = sorted(self.epochs, key=itemgetter('loss')) best_epoch = sorted_epochs[0] - self.print_epoch_details(best_epoch, self.total_epochs, self.print_json) + HyperoptTools.print_epoch_details(best_epoch, self.total_epochs, self.print_json) else: # This is printed when Ctrl+C is pressed quickly, before first epochs have # a chance to be evaluated. diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py new file mode 100644 index 000000000..d4c347f80 --- /dev/null +++ b/freqtrade/optimize/hyperopt_tools.py @@ -0,0 +1,294 @@ + +import io +import logging +from collections import OrderedDict +from pathlib import Path +from pprint import pformat +from typing import Dict, List + +import rapidjson +import tabulate +from colorama import Fore, Style +from joblib import load +from pandas import isna, json_normalize + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import round_dict + + +logger = logging.getLogger(__name__) + + +class HyperoptTools(): + + @staticmethod + def _read_results(results_file: Path) -> List: + """ + Read hyperopt results from file + """ + logger.info("Reading epochs from '%s'", results_file) + data = load(results_file) + return data + + @staticmethod + def load_previous_results(results_file: Path) -> List: + """ + Load data for epochs from the file if we have one + """ + epochs: List = [] + if results_file.is_file() and results_file.stat().st_size > 0: + epochs = HyperoptTools._read_results(results_file) + # Detection of some old format, without 'is_best' field saved + if epochs[0].get('is_best') is None: + raise OperationalException( + "The file with HyperoptTools results is incompatible with this version " + "of Freqtrade and cannot be loaded.") + logger.info(f"Loaded {len(epochs)} previous evaluations from disk.") + return epochs + + @staticmethod + def print_epoch_details(results, total_epochs: int, print_json: bool, + no_header: bool = False, header_str: str = None) -> None: + """ + Display details of the hyperopt result + """ + params = results.get('params_details', {}) + + # Default header string + if header_str is None: + header_str = "Best result" + + if not no_header: + explanation_str = HyperoptTools._format_explanation_string(results, total_epochs) + print(f"\n{header_str}:\n\n{explanation_str}\n") + + if print_json: + result_dict: Dict = {} + for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: + HyperoptTools._params_update_for_json(result_dict, params, s) + print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) + + else: + HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:") + HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:") + HyperoptTools._params_pretty_print(params, 'roi', "ROI table:") + HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:") + HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:") + + @staticmethod + def _params_update_for_json(result_dict, params, space: str) -> None: + if space in params: + space_params = HyperoptTools._space_params(params, space) + if space in ['buy', 'sell']: + result_dict.setdefault('params', {}).update(space_params) + elif space == 'roi': + # TODO: get rid of OrderedDict when support for python 3.6 will be + # dropped (dicts keep the order as the language feature) + + # Convert keys in min_roi dict to strings because + # rapidjson cannot dump dicts with integer keys... + # OrderedDict is used to keep the numeric order of the items + # in the dict. + result_dict['minimal_roi'] = OrderedDict( + (str(k), v) for k, v in space_params.items() + ) + else: # 'stoploss', 'trailing' + result_dict.update(space_params) + + @staticmethod + def _params_pretty_print(params, space: str, header: str) -> None: + if space in params: + space_params = HyperoptTools._space_params(params, space, 5) + params_result = f"\n# {header}\n" + if space == 'stoploss': + params_result += f"stoploss = {space_params.get('stoploss')}" + elif space == 'roi': + # TODO: get rid of OrderedDict when support for python 3.6 will be + # dropped (dicts keep the order as the language feature) + minimal_roi_result = rapidjson.dumps( + OrderedDict( + (str(k), v) for k, v in space_params.items() + ), + default=str, indent=4, number_mode=rapidjson.NM_NATIVE) + params_result += f"minimal_roi = {minimal_roi_result}" + elif space == 'trailing': + + for k, v in space_params.items(): + params_result += f'{k} = {v}\n' + + else: + params_result += f"{space}_params = {pformat(space_params, indent=4)}" + params_result = params_result.replace("}", "\n}").replace("{", "{\n ") + + params_result = params_result.replace("\n", "\n ") + print(params_result) + + @staticmethod + def _space_params(params, space: str, r: int = None) -> Dict: + d = params[space] + # Round floats to `r` digits after the decimal point if requested + return round_dict(d, r) if r else d + + @staticmethod + def is_best_loss(results, current_best_loss: float) -> bool: + return results['loss'] < current_best_loss + + @staticmethod + def _format_explanation_string(results, total_epochs) -> str: + return (("*" if results['is_initial_point'] else " ") + + f"{results['current_epoch']:5d}/{total_epochs}: " + + f"{results['results_explanation']} " + + f"Objective: {results['loss']:.5f}") + + @staticmethod + def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool, + print_colorized: bool, remove_header: int) -> str: + """ + Log result table + """ + if not results: + return '' + + tabulate.PRESERVE_WHITESPACE = True + + trials = json_normalize(results, max_level=1) + trials['Best'] = '' + if 'results_metrics.winsdrawslosses' not in trials.columns: + # Ensure compatibility with older versions of hyperopt results + trials['results_metrics.winsdrawslosses'] = 'N/A' + + trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.winsdrawslosses', + 'results_metrics.avg_profit', 'results_metrics.total_profit', + 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best']] + trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', + 'Total profit', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] + trials['is_profit'] = False + trials.loc[trials['is_initial_point'], 'Best'] = '* ' + trials.loc[trials['is_best'], 'Best'] = 'Best' + trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' + trials.loc[trials['Total profit'] > 0, 'is_profit'] = True + trials['Trades'] = trials['Trades'].astype(str) + + trials['Epoch'] = trials['Epoch'].apply( + lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) + ) + trials['Avg profit'] = trials['Avg profit'].apply( + lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + ) + trials['Avg duration'] = trials['Avg duration'].apply( + lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + ) + trials['Objective'] = trials['Objective'].apply( + lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') + ) + + trials['Profit'] = trials.apply( + lambda x: '{:,.8f} {} {}'.format( + x['Total profit'], config['stake_currency'], + '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') + ).rjust(25+len(config['stake_currency'])) + if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), + axis=1 + ) + trials = trials.drop(columns=['Total profit']) + + if print_colorized: + for i in range(len(trials)): + if trials.loc[i]['is_profit']: + for j in range(len(trials.loc[i])-3): + trials.iat[i, j] = "{}{}{}".format(Fore.GREEN, + str(trials.loc[i][j]), Fore.RESET) + if trials.loc[i]['is_best'] and highlight_best: + for j in range(len(trials.loc[i])-3): + trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT, + str(trials.loc[i][j]), Style.RESET_ALL) + + trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) + if remove_header > 0: + table = tabulate.tabulate( + trials.to_dict(orient='list'), tablefmt='orgtbl', + headers='keys', stralign="right" + ) + + table = table.split("\n", remove_header)[remove_header] + elif remove_header < 0: + table = tabulate.tabulate( + trials.to_dict(orient='list'), tablefmt='psql', + headers='keys', stralign="right" + ) + table = "\n".join(table.split("\n")[0:remove_header]) + else: + table = tabulate.tabulate( + trials.to_dict(orient='list'), tablefmt='psql', + headers='keys', stralign="right" + ) + return table + + @staticmethod + def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, + csv_file: str) -> None: + """ + Log result to csv-file + """ + if not results: + return + + # Verification for overwrite + if Path(csv_file).is_file(): + logger.error(f"CSV file already exists: {csv_file}") + return + + try: + io.open(csv_file, 'w+').close() + except IOError: + logger.error(f"Failed to create CSV file: {csv_file}") + return + + trials = json_normalize(results, max_level=1) + trials['Best'] = '' + trials['Stake currency'] = config['stake_currency'] + + base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.avg_profit', 'results_metrics.median_profit', + 'results_metrics.total_profit', + 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best'] + param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] + trials = trials[base_metrics + param_metrics] + + base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', + 'Stake currency', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] + param_columns = list(results[0]['params_dict'].keys()) + trials.columns = base_columns + param_columns + + trials['is_profit'] = False + trials.loc[trials['is_initial_point'], 'Best'] = '*' + trials.loc[trials['is_best'], 'Best'] = 'Best' + trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' + trials.loc[trials['Total profit'] > 0, 'is_profit'] = True + trials['Epoch'] = trials['Epoch'].astype(str) + trials['Trades'] = trials['Trades'].astype(str) + + trials['Total profit'] = trials['Total profit'].apply( + lambda x: '{:,.8f}'.format(x) if x != 0.0 else "" + ) + trials['Profit'] = trials['Profit'].apply( + lambda x: '{:,.2f}'.format(x) if not isna(x) else "" + ) + trials['Avg profit'] = trials['Avg profit'].apply( + lambda x: '{:,.2f}%'.format(x) if not isna(x) else "" + ) + trials['Avg duration'] = trials['Avg duration'].apply( + lambda x: '{:,.1f} m'.format(x) if not isna(x) else "" + ) + trials['Objective'] = trials['Objective'].apply( + lambda x: '{:,.5f}'.format(x) if x != 100000 else "" + ) + + trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) + trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8') + logger.info(f"CSV file created: {csv_file}") diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 27875ac94..e21ef4dd1 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -920,7 +920,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): mocker.patch( - 'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results', + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', MagicMock(return_value=hyperopt_results) ) @@ -1152,7 +1152,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): def test_hyperopt_show(mocker, capsys, hyperopt_results): mocker.patch( - 'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results', + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', MagicMock(return_value=hyperopt_results) ) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 9ebdad2b5..193d997db 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -16,6 +16,7 @@ from freqtrade.commands.optimize_commands import setup_optimize_configuration, s from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, @@ -336,9 +337,9 @@ def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> Non def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) results_file = testdatadir / 'optimize' / 'ut_results.pickle' - hyperopt_epochs = hyperopt._read_results(results_file) + hyperopt_epochs = HyperoptTools._read_results(results_file) assert log_has(f"Reading epochs from '{results_file}'", caplog) assert hyperopt_epochs == epochs mock_load.assert_called_once() @@ -346,7 +347,7 @@ def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> N def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) statmock = MagicMock() statmock.st_size = 5 @@ -354,16 +355,16 @@ def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: results_file = testdatadir / 'optimize' / 'ut_results.pickle' - hyperopt_epochs = hyperopt.load_previous_results(results_file) + hyperopt_epochs = HyperoptTools.load_previous_results(results_file) assert hyperopt_epochs == epochs mock_load.assert_called_once() del epochs[0]['is_best'] - mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) with pytest.raises(OperationalException): - hyperopt.load_previous_results(results_file) + HyperoptTools.load_previous_results(results_file) def test_roi_table_generation(hyperopt) -> None: @@ -453,7 +454,7 @@ def test_format_results(hyperopt): 'is_initial_point': True, } - result = hyperopt._format_explanation_string(results, 1) + result = HyperoptTools._format_explanation_string(results, 1) assert result.find(' 66.67%') assert result.find('Total profit 1.00000000 BTC') assert result.find('2.0000Σ %') @@ -467,7 +468,7 @@ def test_format_results(hyperopt): df = pd.DataFrame.from_records(trades, columns=labels) results_metrics = hyperopt._calculate_results_metrics(df) results['total_profit'] = results_metrics['total_profit'] - result = hyperopt._format_explanation_string(results, 1) + result = HyperoptTools._format_explanation_string(results, 1) assert result.find('Total profit 1.00000000 EUR') @@ -1076,7 +1077,7 @@ def test_print_epoch_details(capsys): 'is_best': True } - Hyperopt.print_epoch_details(test_result, 5, False, no_header=True) + HyperoptTools.print_epoch_details(test_result, 5, False, no_header=True) captured = capsys.readouterr() assert '# Trailing stop:' in captured.out # re.match(r"Pairs for .*", captured.out) From 983c0ef118e5ee6a63d91478e051477300616fcf Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Thu, 18 Mar 2021 09:47:03 +0900 Subject: [PATCH 536/563] update stoploss_from_open examples to use helper function --- docs/strategy-advanced.md | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index cda988acd..ddf845fca 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -181,17 +181,9 @@ class AwesomeStrategy(IStrategy): #### Calculating stoploss relative to open price -Stoploss values returned from `custom_stoploss` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. -This can be calculated as: - -``` python -def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: - return 1-((1+open_relative_stop)/(1+current_profit)) - -``` - -For example, say our open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, 0.21)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. +The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. #### Trailing stoploss with positive offset @@ -201,9 +193,7 @@ Use the initial stoploss until the profit is above 4%, then use a trailing stopl ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade - -def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: - return 1-((1+open_relative_stop)/(1+current_profit)) +from freqtrade.strategy import stoploss_from_open class AwesomeStrategy(IStrategy): @@ -237,9 +227,7 @@ Instead of continuously trailing behind the current price, this example sets fix ``` python from datetime import datetime from freqtrade.persistence import Trade - -def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: - return 1-((1+open_relative_stop)/(1+current_profit)) +from freqtrade.strategy import stoploss_from_open class AwesomeStrategy(IStrategy): @@ -290,7 +278,7 @@ class AwesomeStrategy(IStrategy): # using current_time directly (like below) will only work in backtesting. # so check "runmode" to make sure that it's only used in backtesting/hyperopt if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'): - relative_sl = self.custom_info[pair].loc[current_time]['atr] + relative_sl = self.custom_info[pair].loc[current_time]['atr'] # in live / dry-run, it'll be really the current time else: # but we can just use the last entry from an already analyzed dataframe instead From b6e9e74a8b3eb9c2efcf50350555783da6fe104a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Mar 2021 06:46:08 +0100 Subject: [PATCH 537/563] Add link between stoploss_from_open and custom_stop documentation --- docs/strategy-advanced.md | 4 +--- docs/strategy-customization.md | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index ddf845fca..4e8ecb67e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -178,10 +178,9 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` - #### Calculating stoploss relative to open price -Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. @@ -189,7 +188,6 @@ The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_ Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. - ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index bf086bc0a..a00928a67 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -600,9 +600,9 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati ``` python - from freqtrade.strategy import IStrategy, stoploss_from_open from datetime import datetime from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, stoploss_from_open class AwesomeStrategy(IStrategy): @@ -621,6 +621,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati ``` + Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. ## Additional data (Wallets) From bf14796d4ceb372545ab9f71844c82c6f6a97ed2 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Thu, 18 Mar 2021 21:50:54 +0900 Subject: [PATCH 538/563] revert "Trailing stoploss with positive offset" example as stoploss_from_open() wasn't adding value --- docs/strategy-advanced.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4e8ecb67e..962b750b5 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -178,20 +178,15 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` -#### Calculating stoploss relative to open price - -Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. - -The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. - #### Trailing stoploss with positive offset Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. +Please note that the stoploss can only increase, values lower than the current stoploss are ignored. + ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade -from freqtrade.strategy import stoploss_from_open class AwesomeStrategy(IStrategy): @@ -203,15 +198,21 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: if current_profit < 0.04: - return 1 # return a value bigger than the inital stoploss to keep using the inital stoploss + return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss # After reaching the desired offset, allow the stoploss to trail by half the profit - # Use a minimum of 2.5% and a maximum of 5% - desired_stop_from_open = max(min(current_profit / 2, 0.05), 0.025) + desired_stoploss = current_profit / 2 - return stoploss_from_open(desired_stop_from_open, current_profit) + # Use a minimum of 2.5% and a maximum of 5% + return max(min(desired_stoploss, 0.05), 0.025 ``` +#### Calculating stoploss relative to open price + +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. + #### Stepped stoploss Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. From dd4d1d82d46341f7ee99b18b5f3eb7237051834e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Mar 2021 14:19:33 +0100 Subject: [PATCH 539/563] Update docs/strategy-advanced.md --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 962b750b5..801bc4731 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -204,7 +204,7 @@ class AwesomeStrategy(IStrategy): desired_stoploss = current_profit / 2 # Use a minimum of 2.5% and a maximum of 5% - return max(min(desired_stoploss, 0.05), 0.025 + return max(min(desired_stoploss, 0.05), 0.025) ``` #### Calculating stoploss relative to open price From 4d52732d30b8f55ec683de156c3ee87203398a08 Mon Sep 17 00:00:00 2001 From: Patrick Brunier Date: Thu, 18 Mar 2021 22:38:54 +0100 Subject: [PATCH 540/563] Added a small snippet to give users a descent error message, when their start date is afer the stop date. Also updated the tests. --- freqtrade/configuration/timerange.py | 2 ++ tests/test_timerange.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 32bbd02a0..2075b38c6 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -103,5 +103,7 @@ class TimeRange: stop = int(stops) // 1000 else: stop = int(stops) + if start > stop > 0: + raise Exception('Start date is after stop date for timerange "%s"' % text) return TimeRange(stype[0], stype[1], start, stop) raise Exception('Incorrect syntax for timerange "%s"' % text) diff --git a/tests/test_timerange.py b/tests/test_timerange.py index 5c35535f0..cd10e219f 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -30,6 +30,9 @@ def test_parse_timerange_incorrect(): with pytest.raises(Exception, match=r'Incorrect syntax.*'): TimeRange.parse_timerange('-') + with pytest.raises(Exception, match=r'Start date is after stop date for timerange.*'): + TimeRange.parse_timerange('20100523-20100522') + def test_subtract_start(): x = TimeRange('date', 'date', 1274486400, 1438214400) From 0d5833ed9133ff629423143db9c810051f3abf45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Mar 2021 06:40:04 +0100 Subject: [PATCH 541/563] Use OperationalException for TimeRange errors --- freqtrade/configuration/timerange.py | 7 +++++-- tests/test_timerange.py | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 2075b38c6..6072e296c 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -7,6 +7,8 @@ from typing import Optional import arrow +from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) @@ -104,6 +106,7 @@ class TimeRange: else: stop = int(stops) if start > stop > 0: - raise Exception('Start date is after stop date for timerange "%s"' % text) + raise OperationalException( + f'Start date is after stop date for timerange "{text}"') return TimeRange(stype[0], stype[1], start, stop) - raise Exception('Incorrect syntax for timerange "%s"' % text) + raise OperationalException(f'Incorrect syntax for timerange "{text}"') diff --git a/tests/test_timerange.py b/tests/test_timerange.py index cd10e219f..dcdaad09d 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -3,6 +3,7 @@ import arrow import pytest from freqtrade.configuration import TimeRange +from freqtrade.exceptions import OperationalException def test_parse_timerange_incorrect(): @@ -27,10 +28,11 @@ def test_parse_timerange_incorrect(): timerange = TimeRange.parse_timerange('-1231006505000') assert TimeRange(None, 'date', 0, 1231006505) == timerange - with pytest.raises(Exception, match=r'Incorrect syntax.*'): + with pytest.raises(OperationalException, match=r'Incorrect syntax.*'): TimeRange.parse_timerange('-') - with pytest.raises(Exception, match=r'Start date is after stop date for timerange.*'): + with pytest.raises(OperationalException, + match=r'Start date is after stop date for timerange.*'): TimeRange.parse_timerange('20100523-20100522') From c1f79922700cd51020cab850905ad6d46599a4f4 Mon Sep 17 00:00:00 2001 From: Maycon Maia Vitali Date: Fri, 19 Mar 2021 10:39:45 -0300 Subject: [PATCH 542/563] Added slash to fix a broken formatting On the command table the pipe(|) broke the formatting. --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 833fae1fe..5ecdf8065 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -146,7 +146,7 @@ official commands. You can ask at any moment for help with `/help`. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. -| `/unlock ` | Remove the lock for this pair (or for this lock id). +| `/unlock ` | Remove the lock for this pair (or for this lock id). | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). From fb90901bb3408555331d24e1f46a0144e3afbb55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Mar 2021 20:12:12 +0100 Subject: [PATCH 543/563] Fix telegram table for both rendered and github markdown --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 5ecdf8065..377977892 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -146,7 +146,7 @@ official commands. You can ask at any moment for help with `/help`. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. -| `/unlock ` | Remove the lock for this pair (or for this lock id). +| `/unlock ` | Remove the lock for this pair (or for this lock id). | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). From 7ffe1fd36a230712a1f0eb19cc9005bf9564e231 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 07:21:22 +0100 Subject: [PATCH 544/563] Fix calculation error for min-trade-stake --- docs/configuration.md | 17 +++++++++++++++++ freqtrade/exchange/exchange.py | 8 ++++---- tests/exchange/test_exchange.py | 18 +++++++++++++----- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index ca1e03b0a..573cbfba2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -156,6 +156,23 @@ Values set in the configuration file always overwrite values set in the strategy There are several methods to configure how much of the stake currency the bot will use to enter a trade. All methods respect the [available balance configuration](#available-balance) as explained below. +#### Minimum trade stake + +The minimum stake amount will depend by exchange and pair, and is usually listed in the exchange support pages. +Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.4$. + +The minimum stake amount to buy this pair is therefore `20 * 0.6 ~= 12`. +This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case. + +To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%). + +With a stoploss of 10% - we'd therefore end up with a value of ~13.8$ (`12 * (1 + 0.05 + 0.1)`). + +To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit. + +!!! Warning + Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. + #### Available balance By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade. diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fdb34eb41..6b8261afc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -531,16 +531,16 @@ class Exchange: return None # reserve some percent defined in config (5% default) + stoploss - amount_reserve_percent = 1.0 - self._config.get('amount_reserve_percent', + amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent += stoploss + amount_reserve_percent += abs(stoploss) # it should not be more than 50% - amount_reserve_percent = max(amount_reserve_percent, 0.5) + amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return max(min_stake_amounts) / amount_reserve_percent + return max(min_stake_amounts) * amount_reserve_percent def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict[str, Any]: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8a8c95a62..942ffd4ab 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1,6 +1,7 @@ import copy import logging from datetime import datetime, timedelta, timezone +from math import isclose from random import randint from unittest.mock import MagicMock, Mock, PropertyMock, patch @@ -370,7 +371,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) - assert result == 2 / 0.9 + assert isclose(result, 2 * 1.1) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -382,7 +383,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert result == 2 * 2 / 0.9 + assert isclose(result, 2 * 2 * 1.1) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -394,7 +395,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert result == max(2, 2 * 2) / 0.9 + assert isclose(result, max(2, 2 * 2) * 1.1) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -406,7 +407,14 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert result == max(8, 2 * 2) / 0.9 + assert isclose(result, max(8, 2 * 2) * 1.1) + + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) + assert isclose(result, max(8, 2 * 2) * 1.45) + + # Really big stoploss + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) + assert isclose(result, max(8, 2 * 2) * 1.5) def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: @@ -424,7 +432,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * 1.1, 8) def test_set_sandbox(default_conf, mocker): From 69799532a67fc9732e851c71a500e223d4ffb589 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 08:13:10 +0100 Subject: [PATCH 545/563] Document usage of open_date_utc closes #4580 --- docs/strategy-advanced.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 801bc4731..7fa824a5b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -146,9 +146,9 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. - if current_time - timedelta(minutes=120) > trade.open_date: + if current_time - timedelta(minutes=120) > trade.open_date_utc: return -0.05 - elif current_time - timedelta(minutes=60) > trade.open_date: + elif current_time - timedelta(minutes=60) > trade.open_date_utc: return -0.10 return 1 ``` @@ -317,7 +317,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to The function must return either `True` (cancel order) or `False` (keep order alive). ``` python -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from freqtrade.persistence import Trade class AwesomeStrategy(IStrategy): @@ -331,21 +331,21 @@ class AwesomeStrategy(IStrategy): } def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: - if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5): + if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): return True - elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3): + elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): return True - elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24): + elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): return True return False def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: - if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5): + if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): return True - elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3): + elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): return True - elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24): + elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): return True return False ``` From 066dd72210889776c06726ee54bbd1ae798d1f20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 08:34:15 +0100 Subject: [PATCH 546/563] add orderbook structure documentation --- docs/strategy-customization.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a00928a67..256b28990 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -436,6 +436,26 @@ if self.dp: dataframe['best_ask'] = ob['asks'][0][0] ``` +The orderbook structure is aligned with the order structure from [ccxt](https://github.com/ccxt/ccxt/wiki/Manual#order-book-structure), so the result will look as follows: + +``` js +{ + 'bids': [ + [ price, amount ], // [ float, float ] + [ price, amount ], + ... + ], + 'asks': [ + [ price, amount ], + [ price, amount ], + //... + ], + //... +} +``` + +Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position. + !!! Warning "Warning about backtesting" The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return uptodate values. From fe7f3d9c37f70983fdb55459528378969c1f3d71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 11:48:39 +0100 Subject: [PATCH 547/563] Add price side validation for market orders --- freqtrade/configuration/config_validation.py | 14 +++++++++ tests/test_configuration.py | 32 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index df9f16f3e..b6029b6a5 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -74,6 +74,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: # validating trailing stoploss _validate_trailing_stoploss(conf) + _validate_price_config(conf) _validate_edge(conf) _validate_whitelist(conf) _validate_protections(conf) @@ -95,6 +96,19 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None: raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.") +def _validate_price_config(conf: Dict[str, Any]) -> None: + """ + When using market orders, price sides must be using the "other" side of the price + """ + if (conf['order_types'].get('buy') == 'market' + and conf['bid_strategy'].get('price_side') != 'ask'): + raise OperationalException('Market buy orders require bid_strategy.price_side = "ask".') + + if (conf['order_types'].get('sell') == 'market' + and conf['ask_strategy'].get('price_side') != 'bid'): + raise OperationalException('Market sell orders require ask_strategy.price_side = "bid".') + + def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if conf.get('stoploss') == 0.0: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 6b3df392b..a0824e65c 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -790,6 +790,38 @@ def test_validate_max_open_trades(default_conf): validate_config_consistency(default_conf) +def test_validate_price_side(default_conf): + default_conf['order_types'] = { + "buy": "limit", + "sell": "limit", + "stoploss": "limit", + "stoploss_on_exchange": False, + } + # Default should pass + validate_config_consistency(default_conf) + + conf = deepcopy(default_conf) + conf['order_types']['buy'] = 'market' + with pytest.raises(OperationalException, + match='Market buy orders require bid_strategy.price_side = "ask".'): + validate_config_consistency(conf) + + conf = deepcopy(default_conf) + conf['order_types']['sell'] = 'market' + with pytest.raises(OperationalException, + match='Market sell orders require ask_strategy.price_side = "bid".'): + validate_config_consistency(conf) + + # Validate inversed case + conf = deepcopy(default_conf) + conf['order_types']['sell'] = 'market' + conf['order_types']['buy'] = 'market' + conf['ask_strategy']['price_side'] = 'bid' + conf['bid_strategy']['price_side'] = 'ask' + + validate_config_consistency(conf) + + def test_validate_tsl(default_conf): default_conf['stoploss'] = 0.0 with pytest.raises(OperationalException, match='The config stoploss needs to be different ' From 16a54b3616efa47bd394ddb660a00881d1fda989 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 13:08:02 +0100 Subject: [PATCH 548/563] Don't require non-mandatory arguments --- freqtrade/configuration/config_validation.py | 8 ++++---- tests/test_freqtradebot.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index b6029b6a5..c7e49f33d 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -100,12 +100,12 @@ def _validate_price_config(conf: Dict[str, Any]) -> None: """ When using market orders, price sides must be using the "other" side of the price """ - if (conf['order_types'].get('buy') == 'market' - and conf['bid_strategy'].get('price_side') != 'ask'): + if (conf.get('order_types', {}).get('buy') == 'market' + and conf.get('bid_strategy', {}).get('price_side') != 'ask'): raise OperationalException('Market buy orders require bid_strategy.price_side = "ask".') - if (conf['order_types'].get('sell') == 'market' - and conf['ask_strategy'].get('price_side') != 'bid'): + if (conf.get('order_types', {}).get('sell') == 'market' + and conf.get('ask_strategy', {}).get('price_side') != 'bid'): raise OperationalException('Market sell orders require ask_strategy.price_side = "bid".') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d7d2e19f6..5ef9960ab 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -94,6 +94,7 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: 'stoploss': 'limit', 'stoploss_on_exchange': True, } + conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) assert freqtrade.strategy.order_types['stoploss_on_exchange'] @@ -128,6 +129,7 @@ def test_order_dict_live(default_conf, mocker, caplog) -> None: 'stoploss': 'limit', 'stoploss_on_exchange': True, } + conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) From 73876b61b491b62f1337b9500eb5e926e53247e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 13:33:49 +0100 Subject: [PATCH 549/563] Show potential errors when loading markets --- freqtrade/exchange/exchange.py | 4 ++-- tests/exchange/test_exchange.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6b8261afc..5b6e2b20d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -311,8 +311,8 @@ class Exchange: self._markets = self._api.load_markets() self._load_async_markets() self._last_markets_refresh = arrow.utcnow().int_timestamp - except ccxt.BaseError as e: - logger.warning('Unable to initialize markets. Reason: %s', e) + except ccxt.BaseError: + logger.exception('Unable to initialize markets.') def reload_markets(self) -> None: """Reload markets both sync and async if refresh interval has passed """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 942ffd4ab..3439c7a09 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -498,7 +498,7 @@ def test__load_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) - assert log_has('Unable to initialize markets. Reason: SomeError', caplog) + assert log_has('Unable to initialize markets.', caplog) expected_return = {'ETH/BTC': 'available'} api_mock = MagicMock() From f4e71c1f145a47c3bd104da5f068b22df902642f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 14:02:13 +0100 Subject: [PATCH 550/563] get_buy_rate tests should be sensible --- tests/test_freqtradebot.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5ef9960ab..8f55a8fe6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -839,17 +839,17 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask ('ask', 4, 5, None, 1, 4), # last not available - uses ask ('ask', 4, 5, None, 0, 4), # last not available - uses ask - ('bid', 10, 20, 10, 0.0, 20), # Full bid side - ('bid', 10, 20, 10, 1.0, 10), # Full last side - ('bid', 10, 20, 10, 0.5, 15), # Between bid and last - ('bid', 10, 20, 10, 0.7, 13), # Between bid and last - ('bid', 10, 20, 10, 0.3, 17), # Between bid and last - ('bid', 4, 5, 10, 1.0, 5), # last bigger than bid - ('bid', 4, 5, 10, 0.5, 5), # last bigger than bid - ('bid', 10, 20, None, 0.5, 20), # last not available - uses bid - ('bid', 4, 5, None, 0.5, 5), # last not available - uses bid - ('bid', 4, 5, None, 1, 5), # last not available - uses bid - ('bid', 4, 5, None, 0, 5), # last not available - uses bid + ('bid', 21, 20, 10, 0.0, 20), # Full bid side + ('bid', 21, 20, 10, 1.0, 10), # Full last side + ('bid', 21, 20, 10, 0.5, 15), # Between bid and last + ('bid', 21, 20, 10, 0.7, 13), # Between bid and last + ('bid', 21, 20, 10, 0.3, 17), # Between bid and last + ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid + ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid + ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid + ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid + ('bid', 6, 5, None, 1, 5), # last not available - uses bid + ('bid', 6, 5, None, 0, 5), # last not available - uses bid ]) def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, last, last_ab, expected) -> None: @@ -858,7 +858,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, default_conf['bid_strategy']['price_side'] = side freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - MagicMock(return_value={'ask': ask, 'last': last, 'bid': bid})) + return_value={'ask': ask, 'last': last, 'bid': bid}) assert freqtrade.get_buy_rate('ETH/BTC', True) == expected assert not log_has("Using cached buy rate for ETH/BTC.", caplog) From 43d7f9ac67a16b10edb63b5e48a0bc23bc7e5bb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 14:38:26 +0100 Subject: [PATCH 551/563] Add bid_last_balance parameter to interpolate sell prices closes #3270 --- docs/configuration.md | 3 ++- docs/includes/pricing.md | 4 ++++ freqtrade/constants.py | 6 ++++++ freqtrade/freqtradebot.py | 10 ++++++++-- tests/test_freqtradebot.py | 39 ++++++++++++++++++++++++-------------- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 573cbfba2..eb3351b8f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -62,12 +62,13 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). -| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled). +| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
**Datatype:** Boolean | `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer | `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
**Datatype:** Boolean | `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market)
*Defaults to `0`.*
**Datatype:** Float (as ratio) | `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).
*Defaults to `ask`.*
**Datatype:** String (either `ask` or `bid`). +| `ask_strategy.bid_last_balance` | Interpolate the selling price. More information [below](#sell-price-without-orderbook-enabled). | `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled).
**Datatype:** Boolean | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
**Datatype:** Positive Integer | `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
**Datatype:** Positive Integer diff --git a/docs/includes/pricing.md b/docs/includes/pricing.md index d8a72cc58..bdf27eb20 100644 --- a/docs/includes/pricing.md +++ b/docs/includes/pricing.md @@ -103,6 +103,10 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. +When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. + +The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price. + ### Market order pricing When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f25f6653d..3a2ed98e9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -165,6 +165,12 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'ask'}, + 'bid_last_balance': { + 'type': 'number', + 'minimum': 0, + 'maximum': 1, + 'exclusiveMaximum': False, + }, 'use_order_book': {'type': 'boolean'}, 'order_book_min': {'type': 'integer', 'minimum': 1}, 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c60d65f72..73f4c91be 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -432,7 +432,7 @@ class FreqtradeBot(LoggingMixin): ticker = self.exchange.fetch_ticker(pair) ticker_rate = ticker[bid_strategy['price_side']] if ticker['last'] and ticker_rate > ticker['last']: - balance = self.config['bid_strategy']['ask_last_balance'] + balance = bid_strategy['ask_last_balance'] ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) used_rate = ticker_rate @@ -745,7 +745,13 @@ class FreqtradeBot(LoggingMixin): logger.warning("Sell Price at location from orderbook could not be determined.") raise PricingError from e else: - rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']] + ticker = self.exchange.fetch_ticker(pair) + ticker_rate = ticker[ask_strategy['price_side']] + if ticker['last'] and ticker_rate < ticker['last']: + balance = ask_strategy.get('bid_last_balance', 0.0) + ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last']) + rate = ticker_rate + if rate is None: raise PricingError(f"Sell-Rate for {pair} was empty.") self._sell_rate_cache[pair] = rate diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8f55a8fe6..c1a17164f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4112,22 +4112,33 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o assert log_has('Sell Price at location 1 from orderbook could not be determined.', caplog) -@pytest.mark.parametrize('side,ask,bid,expected', [ - ('bid', 10.0, 11.0, 11.0), - ('bid', 10.0, 11.2, 11.2), - ('bid', 10.0, 11.0, 11.0), - ('bid', 9.8, 11.0, 11.0), - ('bid', 0.0001, 0.002, 0.002), - ('ask', 10.0, 11.0, 10.0), - ('ask', 10.11, 11.2, 10.11), - ('ask', 0.001, 0.002, 0.001), - ('ask', 0.006, 1.0, 0.006), +@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [ + ('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side + ('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side + ('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat + ('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid + ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid + ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid + ('bid', 0.003, 0.002, 0.005, 0.0, 0.002), + ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side + ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side + ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat + ('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask + ('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask + ('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask + ('ask', 10.0, 11.0, 11.0, 0.0, 10.0), + ('ask', 10.11, 11.2, 11.0, 0.0, 10.11), + ('ask', 0.001, 0.002, 11.0, 0.0, 0.001), + ('ask', 0.006, 1.0, 11.0, 0.0, 0.006), ]) -def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, expected) -> None: +def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, + last, last_ab, expected) -> None: caplog.set_level(logging.DEBUG) default_conf['ask_strategy']['price_side'] = side - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'ask': ask, 'bid': bid}) + default_conf['ask_strategy']['bid_last_balance'] = last_ab + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': ask, 'bid': bid, 'last': last}) pair = "ETH/BTC" # Test regular mode @@ -4186,7 +4197,7 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog): default_conf['ask_strategy']['price_side'] = 'ask' pair = "ETH/BTC" mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': None, 'bid': 0.12}) + return_value={'ask': None, 'bid': 0.12, 'last': None}) ft = get_patched_freqtradebot(mocker, default_conf) with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): ft.get_sell_rate(pair, True) @@ -4195,7 +4206,7 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog): assert ft.get_sell_rate(pair, True) == 0.12 # Reverse sides mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': 0.13, 'bid': None}) + return_value={'ask': 0.13, 'bid': None, 'last': None}) with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): ft.get_sell_rate(pair, True) From e315a6a0da4e9e0232f3463f12cff7561a64c32d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 14:58:51 +0100 Subject: [PATCH 552/563] assume "last" can miss from a ticker response closes #4573 --- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index 6558f196f..a0579b196 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -64,7 +64,7 @@ class PriceFilter(IPairList): :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, false if it should be removed """ - if ticker['last'] is None or ticker['last'] == 0: + if ticker.get('last', None) is None or ticker.get('last') == 0: self.log_once(f"Removed {pair} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).", logger.info) From ac7a1305cbb78d588bbb1d0849370828b17219fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:11 +0000 Subject: [PATCH 553/563] Bump ccxt from 1.43.27 to 1.43.89 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.43.27 to 1.43.89. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.43.27...1.43.89) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08f5b9078..7c8841e67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.3 -ccxt==1.43.27 +ccxt==1.43.89 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 From 9612ba34ed0c97e97d681c4726c13bc2f5d69048 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:17 +0000 Subject: [PATCH 554/563] Bump urllib3 from 1.26.3 to 1.26.4 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.3 to 1.26.4. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.3...1.26.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08f5b9078..0554f326b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 requests==2.25.1 -urllib3==1.26.3 +urllib3==1.26.4 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 From 09c7ee9e923b936cd8e236b7253c08ec4d58a309 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:28 +0000 Subject: [PATCH 555/563] Bump isort from 5.7.0 to 5.8.0 Bumps [isort](https://github.com/pycqa/isort) from 5.7.0 to 5.8.0. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.7.0...5.8.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4f0ea7706..02f7fbca8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ pytest-asyncio==0.14.0 pytest-cov==2.11.1 pytest-mock==3.5.1 pytest-random-order==1.0.4 -isort==5.7.0 +isort==5.8.0 # Convert jupyter notebooks to markdown documents nbconvert==6.0.7 From ea3012e94d78c5119ba3b457a4799f2b714aafee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:35 +0000 Subject: [PATCH 556/563] Bump sqlalchemy from 1.3.23 to 1.4.2 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.23 to 1.4.2. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08f5b9078..c714a8f1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.43.27 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 -SQLAlchemy==1.3.23 +SQLAlchemy==1.4.2 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From e39cff522d33e9a529045ec58610e87362a37328 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Mar 2021 17:30:16 +0100 Subject: [PATCH 557/563] Remove duplicate dict keys in test --- tests/rpc/test_rpc_apiserver.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 01492b4f2..5a0a04943 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -810,14 +810,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'stoploss_entry_dist_ratio': -0.10448878, 'trade_id': 1, 'close_rate_requested': None, - 'current_rate': 1.099e-05, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, 'fee_open': 0.0025, 'fee_open_cost': None, 'fee_open_currency': None, - 'open_date': ANY, 'is_open': True, 'max_rate': 1.099e-05, 'min_rate': 1.098e-05, From b7702a1e9f121764605144d63c2480a2b82b08cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Mar 2021 19:39:06 +0100 Subject: [PATCH 558/563] Improve tests to work with new sqlalchemy version --- tests/test_freqtradebot.py | 2 +- tests/test_persistence.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c1a17164f..486c31090 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2798,7 +2798,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=InvalidOrderException()) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300)) - sellmock = MagicMock() + sellmock = MagicMock(return_value={'id': '12345555'}) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1820250a5..6a388327c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 import logging from datetime import datetime, timedelta, timezone +from pathlib import Path from types import FunctionType from unittest.mock import MagicMock @@ -21,14 +22,15 @@ def test_init_create_session(default_conf): assert 'scoped_session' in type(Trade.session).__name__ -def test_init_custom_db_url(default_conf, mocker): +def test_init_custom_db_url(default_conf, tmpdir): # Update path to a value other than default, but still in-memory - default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) - create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) + filename = f"{tmpdir}/freqtrade2_test.sqlite" + assert not Path(filename).is_file() + + default_conf.update({'db_url': f'sqlite:///{filename}'}) init_db(default_conf['db_url'], default_conf['dry_run']) - assert create_engine_mock.call_count == 1 - assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' + assert Path(filename).is_file() def test_init_invalid_db_url(default_conf): @@ -49,15 +51,16 @@ def test_init_prod_db(default_conf, mocker): assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' -def test_init_dryrun_db(default_conf, mocker): - default_conf.update({'dry_run': True}) - default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) - - create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) +def test_init_dryrun_db(default_conf, tmpdir): + filename = f"{tmpdir}/freqtrade2_prod.sqlite" + assert not Path(filename).is_file() + default_conf.update({ + 'dry_run': True, + 'db_url': f'sqlite:///{filename}' + }) init_db(default_conf['db_url'], default_conf['dry_run']) - assert create_engine_mock.call_count == 1 - assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite' + assert Path(filename).is_file() @pytest.mark.usefixtures("init_persistence") From 4e8999ade3e8e9d39a5e78d175b736ed6d6b7dc1 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Mon, 22 Mar 2021 20:40:11 +0100 Subject: [PATCH 559/563] Changed the code for status table a bit so that it splits up the trades per 50 trades, to make sure it can be sent regardless of number of trades Signed-off-by: Erwin Hoeckx --- freqtrade/rpc/telegram.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7ec67e5d0..2d753db70 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -330,8 +330,12 @@ class Telegram(RPCHandler): statlist, head = self._rpc._rpc_status_table( self._config['stake_currency'], self._config.get('fiat_display_currency', '')) - message = tabulate(statlist, headers=head, tablefmt='simple') - self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) + max_trades_per_msg = 50 + for i in range(0, max(int(len(statlist) / max_trades_per_msg), 1)): + message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], + headers=head, + tablefmt='simple') + self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) From d5301b4d6316f822d387ae6d2947cec5a49e316f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 23 Mar 2021 10:53:09 +0100 Subject: [PATCH 560/563] RateLimit should be enabled by default --- config_full.json.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_full.json.example b/config_full.json.example index 8366774c4..717797933 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -113,7 +113,7 @@ "password": "", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { - "enableRateLimit": false, + "enableRateLimit": true, "rateLimit": 500, "aiohttp_trust_env": false }, From c928cd38dc2e5a67fcf70d7a76d350f5b7549560 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 23 Mar 2021 16:45:42 +0100 Subject: [PATCH 561/563] Small bugfix to make sure it shows all the trades Signed-off-by: Erwin Hoeckx --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2d753db70..b83cbf1a2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,7 +331,7 @@ class Telegram(RPCHandler): self._config['stake_currency'], self._config.get('fiat_display_currency', '')) max_trades_per_msg = 50 - for i in range(0, max(int(len(statlist) / max_trades_per_msg), 1)): + for i in range(0, max(int(len(statlist) / max_trades_per_msg) + 1, 1)): message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], headers=head, tablefmt='simple') From 65a9763fa587aa42823956cd4e207f08905e7906 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 23 Mar 2021 16:54:38 +0100 Subject: [PATCH 562/563] Fixed an issue when there were exactly 50 trades, it was sending an extra empty table Signed-off-by: Erwin Hoeckx --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b83cbf1a2..61a0188cb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,7 +331,7 @@ class Telegram(RPCHandler): self._config['stake_currency'], self._config.get('fiat_display_currency', '')) max_trades_per_msg = 50 - for i in range(0, max(int(len(statlist) / max_trades_per_msg) + 1, 1)): + for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)): message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], headers=head, tablefmt='simple') From 2fd510e6e4e8a2108f2a64c6ccad72f83fb047d7 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 23 Mar 2021 21:52:46 +0100 Subject: [PATCH 563/563] Added comment with an example calculation Signed-off-by: Erwin Hoeckx --- freqtrade/rpc/telegram.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 61a0188cb..2063a4f58 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,6 +331,11 @@ class Telegram(RPCHandler): self._config['stake_currency'], self._config.get('fiat_display_currency', '')) max_trades_per_msg = 50 + """ + Calculate the number of messages of 50 trades per message + 0.99 is used to make sure that there are no extra (empty) messages + As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message + """ for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)): message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], headers=head,