diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fe1ad853..7b077be04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -351,7 +351,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@v1.5.0 if: (github.event_name == 'release') with: user: __token__ @@ -359,7 +359,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@v1.5.0 if: (github.event_name == 'release') with: user: __token__ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 59e7f6894..a23181c37 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.2.1 - types-filelock==3.2.7 - - types-requests==2.28.0 + - types-requests==2.28.1 - types-tabulate==0.8.11 - types-python-dateutil==2.8.18 # stages: [push] diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index 6382e1baf..e2e9a16fd 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -155,7 +155,8 @@ "entry_cancel": "on", "exit_cancel": "on", "protection_trigger": "off", - "protection_trigger_global": "on" + "protection_trigger_global": "on", + "show_candle": "off" }, "reload": true, "balance_dust_level": 0.01 diff --git a/docs/hyperopt.md b/docs/hyperopt.md index f44a68037..55fe8f008 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -335,6 +335,7 @@ There are four parameter types each suited for different purposes. ## Optimizing an indicator parameter Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy. +By default, we assume a stoploss of 5% - and a take-profit (`minimal_roi`) of 10% - which means freqtrade will sell the trade once 10% profit has been reached. ``` python from pandas import DataFrame @@ -349,6 +350,9 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class MyAwesomeStrategy(IStrategy): stoploss = -0.05 timeframe = '15m' + minimal_roi = { + "0": 0.10 + }, # Define the parameter spaces buy_ema_short = IntParameter(3, 50, default=5) buy_ema_long = IntParameter(15, 200, default=50) @@ -383,7 +387,7 @@ class MyAwesomeStrategy(IStrategy): return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - conditions = [] + conditions = [] conditions.append(qtpylib.crossed_above( dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}'] )) @@ -404,7 +408,7 @@ Using `self.buy_ema_short.range` will return a range object containing all entri In this case (`IntParameter(3, 50, default=5)`), the loop would run for all numbers between 3 and 50 (`[3, 4, 5, ... 49, 50]`). By using this in a loop, hyperopt will generate 48 new columns (`['buy_ema_3', 'buy_ema_4', ... , 'buy_ema_50']`). -Hyperopt itself will then use the selected value to create the buy and sell signals +Hyperopt itself will then use the selected value to create the buy and sell signals. While this strategy is most likely too simple to provide consistent profit, it should serve as an example how optimize indicator parameters. @@ -868,6 +872,22 @@ To combat these, you have multiple options: * reduce the number of parallel processes (`-j `) * Increase the memory of your machine +## The objective has been evaluated at this point before. + +If you see `The objective has been evaluated at this point before.` - then this is a sign that your space has been exhausted, or is close to that. +Basically all points in your space have been hit (or a local minima has been hit) - and hyperopt does no longer find points in the multi-dimensional space it did not try yet. +Freqtrade tries to counter the "local minima" problem by using new, randomized points in this case. + +Example: + +``` python +buy_ema_short = IntParameter(5, 20, default=10, space="buy", optimize=True) +# This is the only parameter in the buy space +``` + +The `buy_ema_short` space has 15 possible values (`5, 6, ... 19, 20`). If you now run hyperopt for the buy space, hyperopt will only have 15 values to try before running out of options. +Your epochs should therefore be aligned to the possible values - or you should be ready to interrupt a run if you norice a lot of `The objective has been evaluated at this point before.` warnings. + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 66e83a397..22d92c65d 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,4 @@ +markdown==3.3.7 mkdocs==1.3.0 mkdocs-material==8.3.9 mdx_truly_sane_lists==1.2 diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 374c675a2..a3115bfb2 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -224,3 +224,5 @@ for val in self.buy_ema_short.range: # Append columns to existing dataframe merged_frame = pd.concat(frames, axis=1) ``` + +Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant. diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index ae0c6a6a3..fbfce37d1 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -31,11 +31,13 @@ pair = "BTC/USDT" ```python # Load data using values set above from freqtrade.data.history import load_pair_history +from freqtrade.enums import CandleType candles = load_pair_history(datadir=data_location, timeframe=config["timeframe"], pair=pair, data_format = "hdf5", + candle_type=CandleType.SPOT, ) # Confirm success @@ -93,7 +95,7 @@ from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats # if backtest_dir points to a directory, it'll automatically load the last backtest file. backtest_dir = config["user_data_dir"] / "backtest_results" -# backtest_dir can also point to a specific file +# backtest_dir can also point to a specific file # backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json" ``` diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 2145797b4..9853e15c6 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -97,7 +97,8 @@ Example configuration showing the different settings: "entry_fill": "off", "exit_fill": "off", "protection_trigger": "off", - "protection_trigger_global": "on" + "protection_trigger_global": "on", + "show_candle": "off" }, "reload": true, "balance_dust_level": 0.01 @@ -108,7 +109,7 @@ Example configuration showing the different settings: `exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange. `*_fill` notifications are off by default and must be explicitly enabled. `protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered. - +`show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc". `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. `reload` allows you to disable reload-buttons on selected messages. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 18dbea259..6d74ceafd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -313,6 +313,10 @@ CONF_SCHEMA = { 'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS, }, + 'show_candle': { + 'type': 'string', + 'enum': ['off', 'ohlc'], + }, } }, 'reload': {'type': 'boolean'}, @@ -538,3 +542,4 @@ TradeList = List[List] LongShort = Literal['long', 'short'] EntryExit = Literal['entry', 'exit'] BuySell = Literal['buy', 'sell'] +MakerTaker = Literal['maker', 'taker'] diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 0046ba458..5765dc459 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -73,7 +73,7 @@ EXCHANGE_HAS_REQUIRED = [ EXCHANGE_HAS_OPTIONAL = [ # Private 'fetchMyTrades', # Trades for order - fee detection - # 'createLimitOrder', 'createMarketOrder', # Either OR for orders + 'createLimitOrder', 'createMarketOrder', # Either OR for orders # 'setLeverage', # Margin/Futures trading # 'setMarginMode', # Margin/Futures trading # 'fetchFundingHistory', # Futures trading diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7ae0a883d..3bd022089 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -20,7 +20,7 @@ from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, Precise, decimal_to_ from pandas import DataFrame from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell, - EntryExit, ListPairsWithTimeframes, PairWithTimeframe) + EntryExit, ListPairsWithTimeframes, MakerTaker, PairWithTimeframe) from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, @@ -79,6 +79,7 @@ class Exchange: "ccxt_futures_name": "swap", "fee_cost_in_contracts": False, # Fee cost needs contract conversion "needs_trading_fees": False, # use fetch_trading_fees to cache fees + "order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'], } _ft_has: Dict = {} _ft_has_futures: Dict = {} @@ -175,23 +176,11 @@ class Exchange: logger.info(f'Using Exchange "{self.name}"') if validate: - # Check if timeframe is available - self.validate_timeframes(config.get('timeframe')) - # Initial markets load self._load_markets() - - # Check if all pairs are available - self.validate_stakecurrency(config['stake_currency']) - if not exchange_config.get('skip_pair_validation'): - 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_config(config) self.required_candle_call_count = self.validate_required_startup_candles( config.get('startup_candle_count', 0), config.get('timeframe', '')) - self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode) - self.validate_pricing(config['exit_pricing']) - self.validate_pricing(config['entry_pricing']) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( @@ -214,6 +203,20 @@ class Exchange: logger.info("Closing async ccxt session.") self.loop.run_until_complete(self._api_async.close()) + def validate_config(self, config): + # Check if timeframe is available + self.validate_timeframes(config.get('timeframe')) + + # Check if all pairs are available + self.validate_stakecurrency(config['stake_currency']) + if not config['exchange'].get('skip_pair_validation'): + 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_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode) + self.validate_pricing(config['exit_pricing']) + self.validate_pricing(config['entry_pricing']) + def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt, ccxt_kwargs: Dict = {}) -> ccxt.Exchange: """ @@ -423,7 +426,7 @@ class Exchange: if 'symbol' in order and order['symbol'] is not None: contract_size = self._get_contract_size(order['symbol']) if contract_size != 1: - for prop in ['amount', 'cost', 'filled', 'remaining']: + for prop in self._ft_has.get('order_props_in_contracts', []): if prop in order and order[prop] is not None: order[prop] = order[prop] * contract_size return order @@ -586,13 +589,10 @@ class Exchange: """ Checks if order-types configured in strategy/config are supported """ - # TODO: Reenable once ccxt fixes createMarketOrder assignment - as well as - # Revert the change in test_validate_ordertypes. - - # if any(v == 'market' for k, v in order_types.items()): - # if not self.exchange_has('createMarketOrder'): - # raise OperationalException( - # f'Exchange {self.name} does not support market orders.') + if any(v == 'market' for k, v in order_types.items()): + if not self.exchange_has('createMarketOrder'): + raise OperationalException( + f'Exchange {self.name} does not support market orders.') if (order_types.get("stoploss_on_exchange") and not self._ft_has.get("stoploss_on_exchange", False)): @@ -824,7 +824,7 @@ class Exchange: 'price': rate, 'average': rate, 'amount': _amount, - 'cost': _amount * rate / leverage, + 'cost': _amount * rate, 'type': ordertype, 'side': side, 'filled': 0, @@ -850,20 +850,27 @@ class Exchange: 'filled': _amount, 'cost': (dry_order['amount'] * average) / leverage }) - dry_order = self.add_dry_order_fee(pair, dry_order) + # market orders will always incurr taker fees + dry_order = self.add_dry_order_fee(pair, dry_order, 'taker') - dry_order = self.check_dry_limit_order_filled(dry_order) + dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True) self._dry_run_open_orders[dry_order["id"]] = dry_order # Copy order and close it - so the returned order is open unless it's a market order return dry_order - def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]) -> Dict[str, Any]: + def add_dry_order_fee( + self, + pair: str, + dry_order: Dict[str, Any], + taker_or_maker: MakerTaker, + ) -> Dict[str, Any]: + fee = self.get_fee(pair, taker_or_maker=taker_or_maker) dry_order.update({ 'fee': { 'currency': self.get_pair_quote_currency(pair), - 'cost': dry_order['cost'] * self.get_fee(pair), - 'rate': self.get_fee(pair) + 'cost': dry_order['cost'] * fee, + 'rate': fee } }) return dry_order @@ -929,7 +936,8 @@ class Exchange: pass return False - def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]: + def check_dry_limit_order_filled( + self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]: """ Check dry-run limit order fill and update fee (if it filled). """ @@ -943,7 +951,12 @@ class Exchange: 'filled': order['amount'], 'remaining': 0, }) - self.add_dry_order_fee(pair, order) + + self.add_dry_order_fee( + pair, + order, + 'taker' if immediate else 'maker', + ) return order @@ -1631,7 +1644,7 @@ class Exchange: @retrier def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1, - price: float = 1, taker_or_maker: str = 'maker') -> float: + price: float = 1, taker_or_maker: MakerTaker = 'maker') -> float: try: if self._config['dry_run'] and self._config.get('fee', None) is not None: return self._config['fee'] @@ -1679,7 +1692,7 @@ class Exchange: fee_curr = fee.get('currency') if fee_curr is None: return None - fee_cost = fee['cost'] + fee_cost = float(fee['cost']) if self._ft_has['fee_cost_in_contracts']: # Convert cost via "contracts" conversion fee_cost = self._contracts_to_amount(symbol, fee['cost']) @@ -1687,7 +1700,7 @@ class Exchange: # Calculate fee based on order details if fee_curr == self.get_pair_base_currency(symbol): # Base currency - divide by amount - return round(fee['cost'] / amount, 8) + return round(fee_cost / amount, 8) elif fee_curr == self.get_pair_quote_currency(symbol): # Quote currency - divide by cost return round(fee_cost / cost, 8) if cost else None @@ -1718,7 +1731,7 @@ class Exchange: :param amount: Amount of the order :return: Tuple with cost, currency, rate of the given fee dict """ - return (fee['cost'], + return (float(fee['cost']), fee['currency'], self.calculate_fee_rate( fee, diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index b9de212de..6df3425d2 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -1,12 +1,13 @@ """ Gate.io exchange subclass """ import logging from datetime import datetime -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange +from freqtrade.misc import safe_value_fallback2 logger = logging.getLogger(__name__) @@ -34,6 +35,7 @@ class Gateio(Exchange): _ft_has_futures: Dict = { "needs_trading_fees": True, "fee_cost_in_contracts": False, # Set explicitly to false for clarity + "order_props_in_contracts": ['amount', 'filled', 'remaining'], } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ @@ -96,12 +98,29 @@ class Gateio(Exchange): } return trades + def get_order_id_conditional(self, order: Dict[str, Any]) -> str: + if self.trading_mode == TradingMode.FUTURES: + return safe_value_fallback2(order, order, 'id_stop', 'id') + return order['id'] + def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: - return self.fetch_order( + order = self.fetch_order( order_id=order_id, pair=pair, params={'stop': True} ) + if self.trading_mode == TradingMode.FUTURES: + if order['status'] == 'closed': + # Places a real order - which we need to fetch explicitly. + new_orderid = order.get('info', {}).get('trade_id') + if new_orderid: + order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params) + order1['id_stop'] = order1['id'] + order1['id'] = order_id + order1['stopPrice'] = order.get('stopPrice') + + return order1 + return order def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: return self.cancel_order( diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b0cffcf7e..9bc425cdd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -333,6 +333,8 @@ class FreqtradeBot(LoggingMixin): if not trade.is_open and not trade.fee_updated(trade.exit_side): # Get sell fee order = trade.select_order(trade.exit_side, False) + if not order: + order = trade.select_order('stoploss', False) if order: logger.info( f"Updating {trade.exit_side}-fee on trade {trade}" diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7c7493590..566412f29 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -6,6 +6,7 @@ This module contains the hyperopt logic import logging import random +import sys import warnings from datetime import datetime, timezone from math import ceil @@ -17,6 +18,7 @@ import rapidjson 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 joblib.externals import cloudpickle from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN @@ -87,6 +89,7 @@ class Hyperopt: self.backtesting._set_strategy(self.backtesting.strategylist[0]) self.custom_hyperopt.strategy = self.backtesting.strategy + self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__) self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss( self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function @@ -137,6 +140,17 @@ class Hyperopt: logger.info(f"Removing `{p}`.") p.unlink() + def hyperopt_pickle_magic(self, bases) -> None: + """ + Hyperopt magic to allow strategy inheritance across files. + For this to properly work, we need to register the module of the imported class + to pickle as value. + """ + for modules in bases: + if modules.__name__ != 'IStrategy': + cloudpickle.register_pickle_by_value(sys.modules[modules.__module__]) + self.hyperopt_pickle_magic(modules.__bases__) + def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict: # Ensure the number of dimensions match diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index a64281156..f8e95300a 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -255,18 +255,18 @@ 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 + # Create description for exit summarizing the trade trades['desc'] = trades.apply( lambda row: f"{row['profit_ratio']:.2%}, " + (f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") + f"{row['exit_reason']}, " + f"{row['trade_duration']} min", axis=1) - trade_buys = go.Scatter( + trade_entries = go.Scatter( x=trades["open_date"], y=trades["open_rate"], mode='markers', - name='Trade buy', + name='Trade entry', text=trades["desc"], marker=dict( symbol='circle-open', @@ -277,12 +277,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) ) - trade_sells = go.Scatter( + trade_exits = go.Scatter( 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', + name='Exit - Profit', marker=dict( symbol='square-open', size=11, @@ -290,12 +290,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: color='green' ) ) - trade_sells_loss = go.Scatter( + trade_exits_loss = go.Scatter( 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', + name='Exit - Loss', marker=dict( symbol='square-open', size=11, @@ -303,9 +303,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: color='red' ) ) - fig.add_trace(trade_buys, 1, 1) - fig.add_trace(trade_sells, 1, 1) - fig.add_trace(trade_sells_loss, 1, 1) + fig.add_trace(trade_entries, 1, 1) + fig.add_trace(trade_exits, 1, 1) + fig.add_trace(trade_exits_loss, 1, 1) else: logger.warning("No trades found.") return fig @@ -444,7 +444,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra Generate the graph from the data generated by Backtesting or from DB Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators :param pair: Pair to Display on the graph - :param data: OHLCV DataFrame containing indicators and buy/sell signals + :param data: OHLCV DataFrame containing indicators and entry/exit signals :param trades: All trades created :param indicators1: List containing Main plot indicators :param indicators2: List containing Sub plot indicators diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9ac8ad828..0f045900d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -243,6 +243,22 @@ class Telegram(RPCHandler): """ return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}" + def _add_analyzed_candle(self, pair: str) -> str: + candle_val = self._config['telegram'].get( + 'notification_settings', {}).get('show_candle', 'off') + if candle_val != 'off': + if candle_val == 'ohlc': + analyzed_df, _ = self._rpc._freqtrade.dataprovider.get_analyzed_dataframe( + pair, self._config['timeframe']) + candle = analyzed_df.iloc[-1].squeeze() if len(analyzed_df) > 0 else None + if candle is not None: + return ( + f"*Candle OHLC*: `{candle['open']}, {candle['high']}, " + f"{candle['low']}, {candle['close']}`\n" + ) + + return '' + def _format_entry_msg(self, msg: Dict[str, Any]) -> str: if self._rpc._fiat_converter: msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( @@ -259,6 +275,7 @@ class Telegram(RPCHandler): f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}" f" (#{msg['trade_id']})\n" ) + message += self._add_analyzed_candle(msg['pair']) message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else "" message += f"*Amount:* `{msg['amount']:.8f}`\n" if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0: @@ -322,6 +339,7 @@ class Telegram(RPCHandler): message = ( f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" + f"{self._add_analyzed_candle(msg['pair'])}" f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* " f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" f"{cp_extra}" diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 93e4b83ae..a7430c225 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -51,11 +51,13 @@ "source": [ "# Load data using values set above\n", "from freqtrade.data.history import load_pair_history\n", + "from freqtrade.enums import CandleType\n", "\n", "candles = load_pair_history(datadir=data_location,\n", " timeframe=config[\"timeframe\"],\n", " pair=pair,\n", " data_format = \"hdf5\",\n", + " candle_type=CandleType.SPOT,\n", " )\n", "\n", "# Confirm success\n", diff --git a/requirements-dev.txt b/requirements-dev.txt index f2f77c2ba..3d91f29fd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ flake8-tidy-imports==4.8.0 mypy==0.961 pre-commit==2.20.0 pytest==7.1.2 -pytest-asyncio==0.18.3 +pytest-asyncio==0.19.0 pytest-cov==3.0.0 pytest-mock==3.8.2 pytest-random-order==1.0.4 @@ -24,6 +24,6 @@ nbconvert==6.5.0 # mypy types types-cachetools==5.2.1 types-filelock==3.2.7 -types-requests==2.28.0 +types-requests==2.28.1 types-tabulate==0.8.11 types-python-dateutil==2.8.18 diff --git a/requirements.txt b/requirements.txt index 115ae734e..b27c8f559 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.90.41 +ccxt==1.90.89 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 @@ -12,7 +12,7 @@ arrow==1.2.2 cachetools==4.2.2 requests==2.28.1 urllib3==1.26.10 -jsonschema==4.6.2 +jsonschema==4.7.2 TA-Lib==0.4.24 technical==1.3.0 tabulate==0.8.10 @@ -34,7 +34,7 @@ orjson==3.7.7 sdnotify==0.3.2 # API Server -fastapi==0.78.0 +fastapi==0.79.0 uvicorn==0.18.2 pyjwt==2.4.0 aiofiles==0.8.0 diff --git a/tests/conftest.py b/tests/conftest.py index eaa0c7128..58b5df329 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -112,11 +112,8 @@ def patch_exchange( mock_supported_modes=True ) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 50154bcaf..74106f28b 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -153,6 +153,25 @@ class TestCCXTExchange(): assert isinstance(markets[pair], dict) assert exchange.market_is_spot(markets[pair]) + def test_has_validations(self, exchange): + + exchange, exchangename = exchange + + exchange.validate_ordertypes({ + 'entry': 'limit', + 'exit': 'limit', + 'stoploss': 'limit', + }) + + if exchangename == 'gateio': + # gateio doesn't have market orders on spot + return + exchange.validate_ordertypes({ + 'entry': 'market', + 'exit': 'market', + 'stoploss': 'market', + }) + def test_load_markets_futures(self, exchange_futures): exchange, exchangename = exchange_futures if not exchange: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a23fd8097..00c75e64a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1078,10 +1078,9 @@ def test_validate_ordertypes(default_conf, mocker): 'stoploss': 'market', 'stoploss_on_exchange': False } - # TODO: Revert once createMarketOrder is available again. - # with pytest.raises(OperationalException, - # match=r'Exchange .* does not support market orders.'): - # Exchange(default_conf) + with pytest.raises(OperationalException, + match=r'Exchange .* does not support market orders.'): + Exchange(default_conf) default_conf['order_types'] = { 'entry': 'limit', @@ -1187,7 +1186,58 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name, leverag assert order["symbol"] == "ETH/BTC" assert order["amount"] == 1 assert order["leverage"] == leverage - assert order["cost"] == 1 * 200 / leverage + assert order["cost"] == 1 * 200 + + +@pytest.mark.parametrize('side,is_short,order_reason', [ + ("buy", False, "entry"), + ("sell", False, "exit"), + ("buy", True, "exit"), + ("sell", True, "entry"), +]) +@pytest.mark.parametrize("order_type,price_side,fee", [ + ("limit", "same", 1.0), + ("limit", "other", 2.0), + ("market", "same", 2.0), + ("market", "other", 2.0), +]) +def test_create_dry_run_order_fees( + default_conf, + mocker, + side, + order_type, + is_short, + order_reason, + price_side, + fee, +): + mocker.patch( + 'freqtrade.exchange.Exchange.get_fee', + side_effect=lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 + ) + mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', + return_value=price_side == 'other') + exchange = get_patched_exchange(mocker, default_conf) + + order = exchange.create_dry_run_order( + pair='LTC/USDT', + ordertype=order_type, + side=side, + amount=10, + rate=2.0, + leverage=1.0 + ) + if price_side == 'other' or order_type == 'market': + assert order['fee']['rate'] == fee + return + else: + assert order['fee'] is None + + mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', + return_value=price_side != 'other') + + order1 = exchange.fetch_dry_run_order(order['id']) + assert order1['fee']['rate'] == fee @pytest.mark.parametrize("side,startprice,endprice", [ diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index cbd4776fb..dabdbba65 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -53,6 +53,25 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker): assert fetch_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC' assert fetch_order_mock.call_args_list[0][1]['params'] == {'stop': True} + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + + exchange = get_patched_exchange(mocker, default_conf, id='gateio') + + exchange.fetch_order = MagicMock(return_value={ + 'status': 'closed', + 'id': '1234', + 'stopPrice': 5.62, + 'info': { + 'trade_id': '222555' + } + }) + + exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert exchange.fetch_order.call_count == 2 + assert exchange.fetch_order.call_args_list[0][1]['order_id'] == '1234' + assert exchange.fetch_order.call_args_list[1][1]['order_id'] == '222555' + def test_cancel_stoploss_order_gateio(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='gateio') diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index 8a9e0cbf0..9eb3a88cc 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -18,11 +18,11 @@ def hyperopt_conf(default_conf): 'runmode': RunMode.HYPEROPT, 'strategy': 'HyperoptableStrategy', 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', - 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), - 'epochs': 1, - 'timerange': None, - 'spaces': ['default'], - 'hyperopt_jobs': 1, + 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), + 'epochs': 1, + 'timerange': None, + 'spaces': ['default'], + 'hyperopt_jobs': 1, 'hyperopt_min_trades': 1, }) return hyperconf diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 6912184aa..0b964c54a 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -90,28 +90,6 @@ def load_data_test(what, testdatadir): fill_missing=True)} -def simple_backtest(config, contour, mocker, testdatadir) -> None: - patch_exchange(mocker) - config['timeframe'] = '1m' - backtesting = Backtesting(config) - backtesting._set_strategy(backtesting.strategylist[0]) - - data = load_data_test(contour, testdatadir) - processed = backtesting.strategy.advise_all_indicators(data) - min_date, max_date = get_timerange(processed) - assert isinstance(processed, dict) - results = backtesting.backtest( - processed=processed, - start_date=min_date, - end_date=max_date, - max_open_trades=1, - position_stacking=False, - enable_protections=config.get('enable_protections', False), - ) - # results :: - return results - - # FIX: fixturize this? def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'): data = history.load_data(datadir=datadir, timeframe='1m', pairs=[pair]) @@ -942,6 +920,7 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None: # While this test IS a copy of test_backtest_pricecontours, it's needed to ensure # results do not carry-over to the next run, which is not given by using parametrize. + patch_exchange(mocker) default_conf['protections'] = [ { "method": "CooldownPeriod", @@ -949,6 +928,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad }] default_conf['enable_protections'] = True + default_conf['timeframe'] = '1m' mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) @@ -959,12 +939,27 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad ['sine', 9], ['raise', 10], ] + backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) + # While entry-signals are unrealistic, running backtesting # over and over again should not cause different results for [contour, numres] in tests: # Debug output for random test failure print(f"{contour}, {numres}") - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres + data = load_data_test(contour, testdatadir) + processed = backtesting.strategy.advise_all_indicators(data) + min_date, max_date = get_timerange(processed) + assert isinstance(processed, dict) + results = backtesting.backtest( + processed=processed, + start_date=min_date, + end_date=max_date, + max_open_trades=1, + position_stacking=False, + enable_protections=default_conf.get('enable_protections', False), + ) + assert len(results['results']) == numres @pytest.mark.parametrize('protections,contour,expected', [ @@ -990,7 +985,25 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) # While entry-signals are unrealistic, running backtesting # over and over again should not cause different results - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == expected + + patch_exchange(mocker) + default_conf['timeframe'] = '1m' + backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) + + data = load_data_test(contour, testdatadir) + processed = backtesting.strategy.advise_all_indicators(data) + min_date, max_date = get_timerange(processed) + assert isinstance(processed, dict) + results = backtesting.backtest( + processed=processed, + start_date=min_date, + end_date=max_date, + max_open_trades=1, + position_stacking=False, + enable_protections=default_conf.get('enable_protections', False), + ) + assert len(results['results']) == expected def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 1ad8b33cf..0f615b7a3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,7 +1,7 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 from datetime import datetime, timedelta from pathlib import Path -from unittest.mock import ANY, MagicMock +from unittest.mock import ANY, MagicMock, PropertyMock import pandas as pd import pytest @@ -18,8 +18,8 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IntParameter -from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file) +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, get_markets, log_has, log_has_re, + patch_exchange, patched_configuration_load_config_file) def generate_result_metrics(): @@ -855,7 +855,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: 'strategy': 'HyperoptableStrategy', 'user_data_dir': Path(tmpdir), 'hyperopt_random_state': 42, - 'spaces': ['all'] + 'spaces': ['all'], }) hyperopt = Hyperopt(hyperopt_conf) hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) @@ -883,6 +883,45 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: hyperopt.get_optimizer([], 2) +def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None: + mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch('freqtrade.exchange.Exchange._load_markets') + mocker.patch('freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=get_markets())) + (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + # No hyperopt needed + hyperopt_conf.update({ + 'strategy': 'HyperoptableStrategy', + 'user_data_dir': Path(tmpdir), + 'hyperopt_random_state': 42, + 'spaces': ['all'], + # Enforce parallelity + 'epochs': 2, + 'hyperopt_jobs': 2, + 'fee': fee.return_value, + }) + hyperopt = Hyperopt(hyperopt_conf) + hyperopt.backtesting.exchange.get_max_leverage = lambda *x, **xx: 1.0 + hyperopt.backtesting.exchange.get_min_pair_stake_amount = lambda *x, **xx: 1.0 + hyperopt.backtesting.exchange.get_max_pair_stake_amount = lambda *x, **xx: 100.0 + + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) + assert hyperopt.backtesting.strategy.bot_loop_started is True + + assert hyperopt.backtesting.strategy.buy_rsi.in_space is True + assert hyperopt.backtesting.strategy.buy_rsi.value == 35 + assert hyperopt.backtesting.strategy.sell_rsi.value == 74 + assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30 + buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range + assert isinstance(buy_rsi_range, range) + # Range from 0 - 50 (inclusive) + assert len(list(buy_rsi_range)) == 51 + + hyperopt.start() + + def test_SKDecimal(): space = SKDecimal(1, 2, decimals=2) assert 1.5 in space diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c0de54c6d..57c08f48e 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1398,6 +1398,7 @@ def test_api_strategies(botclient): assert rc.json() == {'strategies': [ 'HyperoptableStrategy', + 'HyperoptableStrategyV2', 'InformativeDecoratorTest', 'StrategyTestV2', 'StrategyTestV3', diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f2e057fd3..bfd89dbee 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -12,6 +12,7 @@ from unittest.mock import ANY, MagicMock import arrow import pytest +from pandas import DataFrame from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram.error import BadRequest, NetworkError, TelegramError @@ -1661,8 +1662,17 @@ def test_show_config_handle(default_conf, update, mocker) -> None: (RPCMessageType.ENTRY, 'Long', 'long_signal_01', 1.0), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', 5.0), (RPCMessageType.ENTRY, 'Short', 'short_signal_01', 2.0)]) -def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, - enter, enter_signal, leverage) -> None: +def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type, + enter, enter_signal, leverage) -> None: + default_conf['telegram']['notification_settings']['show_candle'] = 'ohlc' + df = DataFrame({ + 'open': [1.1], + 'high': [2.2], + 'low': [1.0], + 'close': [1.5], + }) + mocker.patch('freqtrade.data.dataprovider.DataProvider.get_analyzed_dataframe', + return_value=(df, 1)) msg = { 'type': message_type, @@ -1680,6 +1690,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, 'fiat_currency': 'USD', 'current_rate': 1.099e-05, 'amount': 1333.3333333333335, + 'analyzed_candle': {'open': 1.1, 'high': 2.2, 'low': 1.0, 'close': 1.5}, 'open_date': arrow.utcnow().shift(hours=-1) } telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1689,6 +1700,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, assert msg_mock.call_args[0][0] == ( f'\N{LARGE BLUE CIRCLE} *Binance (dry):* {enter} ETH/BTC (#1)\n' + '*Candle OHLC*: `1.1, 2.2, 1.0, 1.5`\n' f'*Enter Tag:* `{enter_signal}`\n' '*Amount:* `1333.33333333`\n' f'{leverage_text}' @@ -1716,7 +1728,8 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, @pytest.mark.parametrize('message_type,enter_signal', [ (RPCMessageType.ENTRY_CANCEL, 'long_signal_01'), (RPCMessageType.ENTRY_CANCEL, 'short_signal_01')]) -def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, enter_signal) -> None: +def test_send_msg_enter_cancel_notification( + default_conf, mocker, message_type, enter_signal) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 876b31b14..9850a5675 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -1,13 +1,13 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement from pandas import DataFrame -from strategy_test_v2 import StrategyTestV2 +from strategy_test_v3 import StrategyTestV3 import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter -class HyperoptableStrategy(StrategyTestV2): +class HyperoptableStrategy(StrategyTestV3): """ Default Strategy provided by freqtrade bot. Please do not modify this strategy, it's intended for internal use only. diff --git a/tests/strategy/strats/hyperoptable_strategy_v2.py b/tests/strategy/strats/hyperoptable_strategy_v2.py new file mode 100644 index 000000000..94a15b456 --- /dev/null +++ b/tests/strategy/strats/hyperoptable_strategy_v2.py @@ -0,0 +1,54 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from strategy_test_v2 import StrategyTestV2 + +from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter + + +class HyperoptableStrategyV2(StrategyTestV2): + """ + Default Strategy provided by freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + + buy_params = { + 'buy_rsi': 35, + # Intentionally not specified, so "default" is tested + # 'buy_plusdi': 0.4 + } + + sell_params = { + 'sell_rsi': 74, + 'sell_minusdi': 0.4 + } + + buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy') + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') + sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', + load=False) + protection_enabled = BooleanParameter(default=True) + protection_cooldown_lookback = IntParameter([0, 50], default=30) + + @property + def protections(self): + prot = [] + if self.protection_enabled.value: + prot.append({ + "method": "CooldownPeriod", + "stop_duration_candles": self.protection_cooldown_lookback.value + }) + return prot + + bot_loop_started = False + + def bot_loop_start(self): + self.bot_loop_started = True + + def bot_start(self, **kwargs) -> None: + """ + Parameters can also be defined here ... + """ + self.buy_rsi = IntParameter([0, 50], default=30, space='buy') diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index dca87e724..f6996a7a2 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -916,7 +916,7 @@ def test_hyperopt_parameters(): def test_auto_hyperopt_interface(default_conf): - default_conf.update({'strategy': 'HyperoptableStrategy'}) + default_conf.update({'strategy': 'HyperoptableStrategyV2'}) PairLocks.timeframe = default_conf['timeframe'] strategy = StrategyResolver.load_strategy(default_conf) strategy.ft_bot_start() diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 666ae2b05..bdfcf3211 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -34,7 +34,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 6 + assert len(strategies) == 7 assert isinstance(strategies[0], dict) @@ -42,10 +42,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 7 + assert len(strategies) == 8 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 6 + assert len([x for x in strategies if x['class'] is not None]) == 7 assert len([x for x in strategies if x['class'] is None]) == 1 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b25af0da6..c01c1db17 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2059,8 +2059,9 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) -> @pytest.mark.parametrize("is_short", [False, True]) def test_update_trade_state_sell( - default_conf_usdt, trades_for_order, limit_order_open, limit_order, is_short, mocker, + default_conf_usdt, trades_for_order, limit_order_open, limit_order, is_short, mocker ): + buy_order = limit_order[entry_side(is_short)] open_order = limit_order_open[exit_side(is_short)] l_order = limit_order[exit_side(is_short)] mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) @@ -2087,6 +2088,9 @@ def test_update_trade_state_sell( leverage=1, is_short=is_short, ) + order = Order.parse_from_ccxt_object(buy_order, 'LTC/ETH', entry_side(is_short)) + trade.orders.append(order) + order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', exit_side(is_short)) trade.orders.append(order) assert order.status == 'open' @@ -2790,6 +2794,7 @@ def test_manage_open_orders_partial( rpc_mock = patch_RPCManager(mocker) open_trade.is_short = is_short open_trade.leverage = leverage + open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' limit_buy_order_old_partial['id'] = open_trade.open_order_id limit_buy_order_old_partial['side'] = 'sell' if is_short else 'buy' limit_buy_canceled = deepcopy(limit_buy_order_old_partial) @@ -2875,6 +2880,7 @@ def test_manage_open_orders_partial_except( limit_buy_order_old_partial_canceled, mocker ) -> None: open_trade.is_short = is_short + open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' rpc_mock = patch_RPCManager(mocker) limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id limit_buy_order_old_partial['id'] = open_trade.open_order_id @@ -3638,7 +3644,7 @@ def test_execute_trade_exit_market_order( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, get_fee=fee, - _is_dry_limit_order_filled=MagicMock(return_value=False), + _is_dry_limit_order_filled=MagicMock(return_value=True), ) patch_whitelist(mocker, default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt) @@ -3654,7 +3660,8 @@ def test_execute_trade_exit_market_order( # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_ticker=ticker_usdt_sell_up + fetch_ticker=ticker_usdt_sell_up, + _is_dry_limit_order_filled=MagicMock(return_value=False), ) freqtrade.config['order_types']['exit'] = 'market' @@ -3667,7 +3674,7 @@ def test_execute_trade_exit_market_order( assert not trade.is_open assert trade.close_profit == profit_ratio - assert rpc_mock.call_count == 3 + assert rpc_mock.call_count == 4 last_msg = rpc_mock.call_args_list[-2][0][0] assert { 'type': RPCMessageType.EXIT, diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 9ee7a75c6..52e96e477 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -72,7 +72,7 @@ def test_add_indicators(default_conf, testdatadir, caplog): strategy = StrategyResolver.load_strategy(default_conf) - # Generate buy/sell signals and indicators + # Generate entry/exit signals and indicators data = strategy.analyze_ticker(data, {'pair': pair}) fig = generate_empty_figure() @@ -113,7 +113,7 @@ def test_add_areas(default_conf, testdatadir, caplog): ind_plain = {"macd": {"fill_to": "macdhist"}} strategy = StrategyResolver.load_strategy(default_conf) - # Generate buy/sell signals and indicators + # Generate entry/exit signals and indicators data = strategy.analyze_ticker(data, {'pair': pair}) fig = generate_empty_figure() @@ -165,24 +165,24 @@ def test_plot_trades(testdatadir, caplog): fig = plot_trades(fig, trades) figure = fig1.layout.figure - # Check buys - color, should be in first graph, ... - trade_buy = find_trace_in_fig_data(figure.data, 'Trade buy') - assert isinstance(trade_buy, go.Scatter) - assert trade_buy.yaxis == 'y' - assert len(trades) == len(trade_buy.x) - assert trade_buy.marker.color == 'cyan' - assert trade_buy.marker.symbol == 'circle-open' - assert trade_buy.text[0] == '3.99%, buy_tag, roi, 15 min' + # Check entry - color, should be in first graph, ... + trade_entries = find_trace_in_fig_data(figure.data, 'Trade entry') + assert isinstance(trade_entries, go.Scatter) + assert trade_entries.yaxis == 'y' + assert len(trades) == len(trade_entries.x) + assert trade_entries.marker.color == 'cyan' + assert trade_entries.marker.symbol == 'circle-open' + assert trade_entries.text[0] == '3.99%, buy_tag, roi, 15 min' - 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_ratio'] > 0]) == len(trade_sell.x) - assert trade_sell.marker.color == 'green' - assert trade_sell.marker.symbol == 'square-open' - assert trade_sell.text[0] == '3.99%, buy_tag, roi, 15 min' + trade_exit = find_trace_in_fig_data(figure.data, 'Exit - Profit') + assert isinstance(trade_exit, go.Scatter) + assert trade_exit.yaxis == 'y' + assert len(trades.loc[trades['profit_ratio'] > 0]) == len(trade_exit.x) + assert trade_exit.marker.color == 'green' + assert trade_exit.marker.symbol == 'square-open' + assert trade_exit.text[0] == '3.99%, buy_tag, roi, 15 min' - trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss') + trade_sell_loss = find_trace_in_fig_data(figure.data, 'Exit - Loss') assert isinstance(trade_sell_loss, go.Scatter) assert trade_sell_loss.yaxis == 'y' assert len(trades.loc[trades['profit_ratio'] <= 0]) == len(trade_sell_loss.x)