diff --git a/config.json.example b/config.json.example index 545fcf1d4..9e3daa2b5 100644 --- a/config.json.example +++ b/config.json.example @@ -76,6 +76,15 @@ "token": "your_telegram_token", "chat_id": "your_telegram_chat_id" }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "info", + "jwt_secret_key": "somethingrandom", + "username": "", + "password": "" + }, "initial_state": "running", "forcebuy_enable": false, "internals": { diff --git a/config_binance.json.example b/config_binance.json.example index 98dada647..b45e69bba 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -81,6 +81,15 @@ "token": "your_telegram_token", "chat_id": "your_telegram_chat_id" }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "info", + "jwt_secret_key": "somethingrandom", + "username": "", + "password": "" + }, "initial_state": "running", "forcebuy_enable": false, "internals": { diff --git a/config_full.json.example b/config_full.json.example index 66b0d6a0d..1fd1b44a5 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -121,6 +121,7 @@ "enabled": false, "listen_ip_address": "127.0.0.1", "listen_port": 8080, + "verbosity": "info", "jwt_secret_key": "somethingrandom", "username": "freqtrader", "password": "SuperSecurePassword" diff --git a/config_kraken.json.example b/config_kraken.json.example index 602e2ccf3..7e4001ff3 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -87,6 +87,15 @@ "token": "your_telegram_token", "chat_id": "your_telegram_chat_id" }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "info", + "jwt_secret_key": "somethingrandom", + "username": "", + "password": "" + }, "initial_state": "running", "forcebuy_enable": false, "internals": { diff --git a/docs/configuration.md b/docs/configuration.md index 4dbcc6d2a..51673de5f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -53,8 +53,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `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 | `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 in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict -| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Float (as ratio) +| `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 +| `stoploss` | **Required.** Value as ratio of the stoploss used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Float (as ratio) | `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Boolean | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [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). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
**Datatype:** Float @@ -103,6 +103,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details.
**Datatype:** Boolean | `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details.
**Datatype:** IPv4 | `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details.
**Datatype:** Integer between 1024 and 65535 +| `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 | `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 @@ -217,7 +218,7 @@ To allow the bot to trade all the available `stake_currency` in your account (mi ### Understand minimal_roi The `minimal_roi` configuration parameter is a JSON object where the key is a duration -in minutes and the value is the minimum ROI in percent. +in minutes and the value is the minimum ROI as ratio. See the example below: ```json diff --git a/docs/edge.md b/docs/edge.md index 28a7f14cb..c91e72a3a 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -148,7 +148,6 @@ Edge module has following configuration options: | `enabled` | If true, then Edge will run periodically.
*Defaults to `false`.*
**Datatype:** Boolean | `process_throttle_secs` | How often should Edge run in seconds.
*Defaults to `3600` (once per hour).*
**Datatype:** Integer | `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy.
**Note** that it downloads historical data so increasing this number would lead to slowing down the bot.
*Defaults to `7`.*
**Datatype:** Integer -| `capital_available_percentage` | **DEPRECATED - [replaced with `tradable_balance_ratio`](configuration.md#Available balance)** This is the percentage of the total capital on exchange in stake currency.
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
*Defaults to `0.5`.*
**Datatype:** Float | `allowed_risk` | Ratio of allowed risk per trade.
*Defaults to `0.01` (1%)).*
**Datatype:** Float | `stoploss_range_min` | Minimum stoploss.
*Defaults to `-0.01`.*
**Datatype:** Float | `stoploss_range_max` | Maximum stoploss.
*Defaults to `-0.10`.*
**Datatype:** Float diff --git a/docs/rest-api.md b/docs/rest-api.md index 7f1a95b12..ed5f355b4 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -11,6 +11,7 @@ Sample configuration: "enabled": true, "listen_ip_address": "127.0.0.1", "listen_port": 8080, + "verbosity": "info", "jwt_secret_key": "somethingrandom", "username": "Freqtrader", "password": "SuperSecret1!" diff --git a/docs/stoploss.md b/docs/stoploss.md index f6d56fd41..0e43817ec 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -1,6 +1,6 @@ # Stop Loss -The `stoploss` configuration parameter is loss in percentage that should trigger a sale. +The `stoploss` configuration parameter is loss as ratio that should trigger a sale. For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. Most of the strategy files already include the optimal `stoploss` value. diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 2d5dba9ca..cefc6ac14 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -60,7 +60,7 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: if (config.get('edge', {}).get('enabled', False) and 'capital_available_percentage' in config.get('edge', {})): - logger.warning( + raise OperationalException( "DEPRECATED: " "Using 'edge.capital_available_percentage' has been deprecated in favor of " "'tradable_balance_ratio'. Please migrate your configuration to " diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3afb4b0f1..6741e0605 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -221,6 +221,7 @@ CONF_SCHEMA = { }, 'username': {'type': 'string'}, 'password': {'type': 'string'}, + 'verbosity': {'type': 'string', 'enum': ['error', 'info']}, }, 'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password'] }, @@ -286,7 +287,6 @@ CONF_SCHEMA = { 'process_throttle_secs': {'type': 'integer', 'minimum': 600}, 'calculate_since_number_of_days': {'type': 'integer'}, 'allowed_risk': {'type': 'number'}, - 'capital_available_percentage': {'type': 'number'}, 'stoploss_range_min': {'type': 'number'}, 'stoploss_range_max': {'type': 'number'}, 'stoploss_range_step': {'type': 'number'}, diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index dd4ea35bb..41252ee51 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -57,9 +57,7 @@ class Edge: if self.config['stake_amount'] != UNLIMITED_STAKE_AMOUNT: raise OperationalException('Edge works only with unlimited stake amount') - # Deprecated capital_available_percentage. Will use tradable_balance_ratio in the future. - self._capital_percentage: float = self.edge_config.get( - 'capital_available_percentage', self.config['tradable_balance_ratio']) + self._capital_ratio: float = self.config['tradable_balance_ratio'] self._allowed_risk: float = self.edge_config.get('allowed_risk') self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14) self._last_updated: int = 0 # Timestamp of pairs last updated time @@ -157,7 +155,7 @@ class Edge: def stake_amount(self, pair: str, free_capital: float, total_capital: float, capital_in_trade: float) -> float: stoploss = self.stoploss(pair) - available_capital = (total_capital + capital_in_trade) * self._capital_percentage + 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) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a8483051e..7d2cc66ff 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -676,6 +676,8 @@ class FreqtradeBot: raise PricingError from e else: rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']] + if rate is None: + raise PricingError(f"Sell-Rate for {pair} was empty.") self._sell_rate_cache[pair] = rate return rate @@ -719,6 +721,9 @@ class FreqtradeBot: raise PricingError from e logger.debug(f" order book {config_ask_strategy['price_side']} top {i}: " f"{sell_rate:0.8f}") + # Assign sell-rate to cache - otherwise sell-rate is never updated in the cache, + # resulting in outdated RPC messages + self._sell_rate_cache[trade.pair] = sell_rate if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 153ce8c80..aa08ee8a7 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -11,7 +11,7 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -def _set_loggers(verbosity: int = 0) -> None: +def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None: """ Set the logging level for third party libraries :return: None @@ -28,6 +28,10 @@ def _set_loggers(verbosity: int = 0) -> None: ) logging.getLogger('telegram').setLevel(logging.INFO) + logging.getLogger('werkzeug').setLevel( + logging.ERROR if api_verbosity == 'error' else logging.INFO + ) + def setup_logging(config: Dict[str, Any]) -> None: """ @@ -77,5 +81,5 @@ def setup_logging(config: Dict[str, Any]) -> None: format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=log_handlers ) - _set_loggers(verbosity) + _set_loggers(verbosity, config.get('api_server', {}).get('verbosity', 'info')) logger.info('Verbosity set to %s', verbosity) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9a48338f1..206279f35 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -18,7 +18,8 @@ from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds -from freqtrade.optimize.optimize_reports import (show_backtest_results, +from freqtrade.optimize.optimize_reports import (generate_backtest_stats, + show_backtest_results, store_backtest_result) from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Trade @@ -411,4 +412,5 @@ class Backtesting: if self.config.get('export', False): store_backtest_result(self.config['exportfilename'], all_results) # Show backtest results - show_backtest_results(self.config, data, all_results) + stats = generate_backtest_stats(self.config, data, all_results) + show_backtest_results(self.config, stats) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 1fc4d721e..c148f0f44 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -18,10 +18,7 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame :param all_results: Dict of Dataframes, one results dataframe per strategy """ for strategy, results in all_results.items(): - records = [(t.pair, t.profit_percent, t.open_time.timestamp(), - t.close_time.timestamp(), t.open_index - 1, t.trade_duration, - t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value) - for index, t in results.iterrows()] + records = backtest_result_to_list(results) if records: filename = recordfilename @@ -34,6 +31,18 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame file_dump_json(filename, records) +def backtest_result_to_list(results: DataFrame) -> List[List]: + """ + Converts a list of Backtest-results to list + :param results: Dataframe containing results for one strategy + :return: List of Lists containing the trades + """ + return [[t.pair, t.profit_percent, t.open_time.timestamp(), + t.close_time.timestamp(), t.open_index - 1, t.trade_duration, + t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value] + for index, t in results.iterrows()] + + def _get_line_floatfmt() -> List[str]: """ Generate floatformat (goes in line with _generate_result_line()) @@ -246,12 +255,13 @@ def generate_edge_table(results: dict) -> str: floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore -def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], - all_results: Dict[str, DataFrame]): +def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], + all_results: Dict[str, DataFrame]): stake_currency = config['stake_currency'] max_open_trades = config['max_open_trades'] - + result: Dict[str, Any] = {'strategy': {}} for strategy, results in all_results.items(): + pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, max_open_trades=max_open_trades, results=results, skip_nan=False) @@ -261,21 +271,43 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], max_open_trades=max_open_trades, results=results.loc[results['open_at_end']], skip_nan=True) + strat_stats = { + 'trades': backtest_result_to_list(results), + 'results_per_pair': pair_results, + 'sell_reason_summary': sell_reason_stats, + 'left_open_trades': left_open_results, + } + result['strategy'][strategy] = strat_stats + + strategy_results = generate_strategy_metrics(stake_currency=stake_currency, + max_open_trades=max_open_trades, + all_results=all_results) + + result['strategy_comparison'] = strategy_results + + return result + + +def show_backtest_results(config: Dict, backtest_stats: Dict): + stake_currency = config['stake_currency'] + + for strategy, results in backtest_stats['strategy'].items(): + # Print results print(f"Result for strategy {strategy}") - table = generate_text_table(pair_results, stake_currency=stake_currency) + table = generate_text_table(results['results_per_pair'], stake_currency=stake_currency) if isinstance(table, str): print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - table = generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats, + table = generate_text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], stake_currency=stake_currency, ) if isinstance(table, str): print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = generate_text_table(left_open_results, stake_currency=stake_currency) + table = generate_text_table(results['left_open_trades'], stake_currency=stake_currency) if isinstance(table, str): print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(table) @@ -283,13 +315,10 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], print('=' * len(table.splitlines()[0])) print() - if len(all_results) > 1: + if len(backtest_stats['strategy']) > 1: # Print Strategy summary table - strategy_results = generate_strategy_metrics(stake_currency=stake_currency, - max_open_trades=max_open_trades, - all_results=all_results) - table = generate_text_table_strategy(strategy_results, stake_currency) + table = generate_text_table_strategy(backtest_stats['strategy_comparison'], stake_currency) print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) print(table) print('=' * len(table.splitlines()[0])) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index f0b97be1e..a3b394769 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -108,7 +108,7 @@ def check_migrate(engine) -> None: sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') # If ticker-interval existed use that, else null. - if has_column(cols, 'ticker_interval'): + if has_column(cols, '): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') else: timeframe = get_column_def(cols, 'timeframe', 'null') @@ -254,48 +254,57 @@ class Trade(_DECL_BASE): 'trade_id': self.id, 'pair': self.pair, 'is_open': self.is_open, + 'exchange': self.exchange, + 'amount': round(self.amount, 8), + 'stake_amount': round(self.stake_amount, 8), + 'strategy': self.strategy, + 'timeframe': self.timeframe, + 'fee_open': self.fee_open, 'fee_open_cost': self.fee_open_cost, 'fee_open_currency': self.fee_open_currency, 'fee_close': self.fee_close, 'fee_close_cost': self.fee_close_cost, 'fee_close_currency': self.fee_close_currency, + 'open_date_hum': arrow.get(self.open_date).humanize(), 'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(self.open_date.timestamp() * 1000), + 'open_rate': self.open_rate, + 'open_rate_requested': self.open_rate_requested, + 'open_trade_price': self.open_trade_price, + 'close_date_hum': (arrow.get(self.close_date).humanize() if self.close_date else None), 'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S") if self.close_date else None), 'close_timestamp': int(self.close_date.timestamp() * 1000) if self.close_date else None, - 'open_rate': self.open_rate, - 'open_rate_requested': self.open_rate_requested, - 'open_trade_price': self.open_trade_price, 'close_rate': self.close_rate, 'close_rate_requested': self.close_rate_requested, - 'amount': round(self.amount, 8), - 'stake_amount': round(self.stake_amount, 8), 'close_profit': self.close_profit, 'close_profit_abs': self.close_profit_abs, + 'sell_reason': self.sell_reason, 'sell_order_status': self.sell_order_status, - 'stop_loss': self.stop_loss, + 'stop_loss': self.stop_loss, # Deprecated - should not be used + 'stop_loss_abs': self.stop_loss, + 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'stoploss_order_id': self.stoploss_order_id, 'stoploss_last_update': (self.stoploss_last_update.strftime("%Y-%m-%d %H:%M:%S") if self.stoploss_last_update else None), 'stoploss_last_update_timestamp': (int(self.stoploss_last_update.timestamp() * 1000) if self.stoploss_last_update else None), - 'initial_stop_loss': self.initial_stop_loss, + 'initial_stop_loss': self.initial_stop_loss, # Deprecated - should not be used + 'initial_stop_loss_abs': self.initial_stop_loss, + 'initial_stop_loss_ratio': (self.initial_stop_loss_pct + if self.initial_stop_loss_pct else None), 'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100 if self.initial_stop_loss_pct else None), 'min_rate': self.min_rate, 'max_rate': self.max_rate, - 'strategy': self.strategy, - 'ticker_interval': self.timeframe, - 'timeframe': self.timeframe, + 'open_order_id': self.open_order_id, - 'exchange': self.exchange, } def adjust_min_max_rates(self, current_price: float) -> None: diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 23b6a85b0..9d0899ccd 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -360,7 +360,6 @@ class ApiServer(RPC): Returns a cumulative profit statistics :return: stats """ - logger.info("LocalRPC - Profit Command Called") stats = self._rpc_trade_statistics(self._config['stake_currency'], self._config.get('fiat_display_currency') @@ -377,8 +376,6 @@ class ApiServer(RPC): Returns a cumulative performance statistics :return: stats """ - logger.info("LocalRPC - performance Command Called") - stats = self._rpc_performance() return self.rest_dump(stats) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e7d4a3be8..5f1ace84a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -532,16 +532,26 @@ class RPC: def _rpc_blacklist(self, add: List[str] = None) -> Dict: """ 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 - and pair not in self._freqtrade.pairlists.blacklist): - self._freqtrade.pairlists.blacklist.append(pair) + if self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency: + if pair not in self._freqtrade.pairlists.blacklist: + self._freqtrade.pairlists.blacklist.append(pair) + else: + errors[pair] = { + 'error_msg': f'Pair {pair} already in pairlist.'} + + else: + errors[pair] = { + 'error_msg': f"Pair {pair} does not match stake currency." + } res = {'method': self._freqtrade.pairlists.name_list, 'length': len(self._freqtrade.pairlists.blacklist), 'blacklist': self._freqtrade.pairlists.blacklist, + 'errors': errors, } return res diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 17354cdb0..4c493e6a0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -539,6 +539,11 @@ class Telegram(RPC): try: blacklist = self._rpc_blacklist(context.args) + errmsgs = [] + for pair, error in blacklist['errors'].items(): + errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`") + if errmsgs: + self._send_msg('\n'.join(errmsgs)) message = f"Blacklist contains {blacklist['length']} pairs\n" message += f"`{', '.join(blacklist['blacklist'])}`" diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index e47c32309..118ae348b 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -53,6 +53,15 @@ "token": "{{ telegram_token }}", "chat_id": "{{ telegram_chat_id }}" }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "info", + "jwt_secret_key": "somethingrandom", + "username": "", + "password": "" + }, "initial_state": "running", "forcebuy_enable": false, "internals": { diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json index b224d7602..4f201f86c 100644 --- a/tests/config_test_comments.json +++ b/tests/config_test_comments.json @@ -92,7 +92,6 @@ "enabled": false, "process_throttle_secs": 3600, "calculate_since_number_of_days": 7, - "capital_available_percentage": 0.5, "allowed_risk": 0.01, "stoploss_range_min": -0.01, "stoploss_range_max": -0.1, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 342d7689f..2eeebff32 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -333,6 +333,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') + mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) @@ -612,8 +613,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker) - mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) - mocker.patch('freqtrade.optimize.backtesting.show_backtest_results', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') + mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') + mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) patched_configuration_load_config_file(mocker, default_conf) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4950c4ea7..9a55c7639 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -82,12 +82,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'current_profit': -0.00408133, 'current_profit_pct': -0.41, 'stop_loss': 0.0, + 'stop_loss_abs': 0.0, 'stop_loss_pct': None, + 'stop_loss_ratio': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss': 0.0, + 'initial_stop_loss_abs': 0.0, 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, 'open_order': '(limit buy rem=0.00000000)', 'exchange': 'bittrex', @@ -137,12 +141,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'current_profit': ANY, 'current_profit_pct': ANY, 'stop_loss': 0.0, + 'stop_loss_abs': 0.0, 'stop_loss_pct': None, + 'stop_loss_ratio': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss': 0.0, + 'initial_stop_loss_abs': 0.0, 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, 'open_order': '(limit buy rem=0.00000000)', 'exchange': 'bittrex', } == results[0] @@ -850,6 +858,20 @@ def test_rpc_blacklist(mocker, default_conf) -> None: assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC'] + ret = rpc._rpc_blacklist(["ETH/BTC"]) + assert 'errors' in ret + assert isinstance(ret['errors'], dict) + assert ret['errors']['ETH/BTC']['error_msg'] == 'Pair ETH/BTC already in pairlist.' + + ret = rpc._rpc_blacklist(["ETH/ETH"]) + 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 '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: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index bccd12e18..b3859c4e6 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -510,8 +510,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'current_profit': -0.00408133, 'current_profit_pct': -0.41, 'current_rate': 1.099e-05, - 'initial_stop_loss': 0.0, - 'initial_stop_loss_pct': None, 'open_date': ANY, 'open_date_hum': 'just now', 'open_timestamp': ANY, @@ -520,10 +518,16 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'pair': 'ETH/BTC', 'stake_amount': 0.001, 'stop_loss': 0.0, + 'stop_loss_abs': 0.0, 'stop_loss_pct': None, + 'stop_loss_ratio': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, + 'initial_stop_loss': 0.0, + 'initial_stop_loss_abs': 0.0, + 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, 'trade_id': 1, 'close_rate_requested': None, 'current_rate': 1.099e-05, @@ -564,7 +568,9 @@ def test_api_blacklist(botclient, mocker): assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], "length": 2, - "method": ["StaticPairList"]} + "method": ["StaticPairList"], + "errors": {}, + } # Add ETH/BTC to blacklist rc = client_post(client, f"{BASE_URI}/blacklist", @@ -572,7 +578,9 @@ def test_api_blacklist(botclient, mocker): assert_response(rc) assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], "length": 3, - "method": ["StaticPairList"]} + "method": ["StaticPairList"], + "errors": {}, + } def test_api_whitelist(botclient): @@ -623,12 +631,11 @@ def test_api_forcebuy(botclient, mocker, fee): data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {'amount': 1, + 'trade_id': None, 'close_date': None, 'close_date_hum': None, 'close_timestamp': None, 'close_rate': 0.265441, - 'initial_stop_loss': None, - 'initial_stop_loss_pct': None, 'open_date': ANY, 'open_date_hum': 'just now', 'open_timestamp': ANY, @@ -636,11 +643,16 @@ def test_api_forcebuy(botclient, mocker, fee): 'pair': 'ETH/ETH', 'stake_amount': 1, 'stop_loss': None, + '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, - 'trade_id': None, + 'initial_stop_loss': None, + 'initial_stop_loss_abs': None, + 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, 'close_profit': None, 'close_profit_abs': None, 'close_rate_requested': None, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 500acaa30..f81127c4c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1087,6 +1087,18 @@ def test_blacklist_static(default_conf, update, mocker) -> None: in msg_mock.call_args_list[0][0][0]) assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"] + msg_mock.reset_mock() + context = MagicMock() + context.args = ["ETH/ETH"] + 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 ("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"] + def test_edge_disabled(default_conf, update, mocker) -> None: msg_mock = MagicMock() diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 9602f6389..689e62ab9 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -654,12 +654,14 @@ def test_set_loggers() -> None: assert logging.getLogger('requests').level is logging.DEBUG assert logging.getLogger('ccxt.base.exchange').level is logging.INFO assert logging.getLogger('telegram').level is logging.INFO + assert logging.getLogger('werkzeug').level is logging.INFO - _set_loggers(verbosity=3) + _set_loggers(verbosity=3, api_verbosity='error') assert logging.getLogger('requests').level is logging.DEBUG assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG assert logging.getLogger('telegram').level is logging.INFO + assert logging.getLogger('werkzeug').level is logging.ERROR @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") @@ -1048,8 +1050,9 @@ def test_process_deprecated_setting_edge(mocker, edge_conf, caplog): 'capital_available_percentage': 0.5, }}) - process_temporary_deprecated_settings(edge_conf) - assert log_has_re(r"DEPRECATED.*Using 'edge.capital_available_percentage'*", caplog) + with pytest.raises(OperationalException, + match=r"DEPRECATED.*Using 'edge.capital_available_percentage'*"): + process_temporary_deprecated_settings(edge_conf) def test_check_conflicting_settings(mocker, default_conf, caplog): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 442917bb6..fb42af723 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3931,6 +3931,28 @@ def test_get_sell_rate_orderbook_exception(default_conf, mocker, caplog): assert log_has("Sell Price at location from orderbook could not be determined.", caplog) +def test_get_sell_rate_exception(default_conf, mocker, caplog): + # Ticker on one side can be empty in certain circumstances. + default_conf['ask_strategy']['price_side'] = 'ask' + pair = "ETH/BTC" + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': None, 'bid': 0.12}) + 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) + + ft.config['ask_strategy']['price_side'] = 'bid' + 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}) + with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): + ft.get_sell_rate(pair, True) + + ft.config['ask_strategy']['price_side'] = 'ask' + assert ft.get_sell_rate(pair, True) == 0.13 + + def test_startup_state(default_conf, mocker): default_conf['pairlist'] = {'method': 'VolumePairList', 'config': {'number_assets': 20} diff --git a/tests/test_persistence.py b/tests/test_persistence.py index bc5b315a1..69ee014c5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -763,12 +763,16 @@ def test_to_json(default_conf, fee): 'sell_reason': None, 'sell_order_status': None, 'stop_loss': None, + 'stop_loss_abs': None, + 'stop_loss_ratio': None, 'stop_loss_pct': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss': None, + 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, 'min_rate': None, 'max_rate': None, 'strategy': None, @@ -806,12 +810,16 @@ def test_to_json(default_conf, fee): 'amount': 100.0, 'stake_amount': 0.001, 'stop_loss': None, + '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': None, + 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, 'close_profit': None, 'close_profit_abs': None, 'close_rate_requested': None,