• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 9394559170

26 Apr 2024 06:36AM UTC coverage: 94.656% (-0.02%) from 94.674%
9394559170

push

github

xmatthias
Loader should be passed as kwarg for clarity

20280 of 21425 relevant lines covered (94.66%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.53
/freqtrade/resolvers/strategy_resolver.py
1
# pragma pylint: disable=attribute-defined-outside-init
2

3
"""
1✔
4
This module load custom strategies
5
"""
6
import logging
1✔
7
import tempfile
1✔
8
from base64 import urlsafe_b64decode
1✔
9
from inspect import getfullargspec
1✔
10
from os import walk
1✔
11
from pathlib import Path
1✔
12
from typing import Any, List, Optional
1✔
13

14
from freqtrade.configuration.config_validation import validate_migrated_strategy_settings
1✔
15
from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES, Config
1✔
16
from freqtrade.enums import TradingMode
1✔
17
from freqtrade.exceptions import OperationalException
1✔
18
from freqtrade.resolvers import IResolver
1✔
19
from freqtrade.strategy.interface import IStrategy
1✔
20

21

22
logger = logging.getLogger(__name__)
1✔
23

24

25
class StrategyResolver(IResolver):
1✔
26
    """
27
    This class contains the logic to load custom strategy class
28
    """
29
    object_type = IStrategy
1✔
30
    object_type_str = "Strategy"
1✔
31
    user_subdir = USERPATH_STRATEGIES
1✔
32
    initial_search_path = None
1✔
33
    extra_path = "strategy_path"
1✔
34

35
    @staticmethod
1✔
36
    def load_strategy(config: Optional[Config] = None) -> IStrategy:
1✔
37
        """
38
        Load the custom class from config parameter
39
        :param config: configuration dictionary or None
40
        """
41
        config = config or {}
1✔
42

43
        if not config.get('strategy'):
1✔
44
            raise OperationalException("No strategy set. Please use `--strategy` to specify "
1✔
45
                                       "the strategy class to use.")
46

47
        strategy_name = config['strategy']
1✔
48
        strategy: IStrategy = StrategyResolver._load_strategy(
1✔
49
            strategy_name, config=config,
50
            extra_dir=config.get('strategy_path'))
51
        strategy.ft_load_params_from_file()
1✔
52
        # Set attributes
53
        # Check if we need to override configuration
54
        #             (Attribute name,                    default,     subkey)
55
        attributes = [("minimal_roi",                     {"0": 10.0}),
1✔
56
                      ("timeframe",                       None),
57
                      ("stoploss",                        None),
58
                      ("trailing_stop",                   None),
59
                      ("trailing_stop_positive",          None),
60
                      ("trailing_stop_positive_offset",   0.0),
61
                      ("trailing_only_offset_is_reached", None),
62
                      ("use_custom_stoploss",             None),
63
                      ("process_only_new_candles",        None),
64
                      ("order_types",                     None),
65
                      ("order_time_in_force",             None),
66
                      ("stake_currency",                  None),
67
                      ("stake_amount",                    None),
68
                      ("protections",                     None),
69
                      ("startup_candle_count",            None),
70
                      ("unfilledtimeout",                 None),
71
                      ("use_exit_signal",                 True),
72
                      ("exit_profit_only",                False),
73
                      ("ignore_roi_if_entry_signal",      False),
74
                      ("exit_profit_offset",              0.0),
75
                      ("disable_dataframe_checks",        False),
76
                      ("ignore_buying_expired_candle_after",  0),
77
                      ("position_adjustment_enable",      False),
78
                      ("max_entry_position_adjustment",      -1),
79
                      ("max_open_trades",                    -1)
80
                      ]
81
        for attribute, default in attributes:
1✔
82
            StrategyResolver._override_attribute_helper(strategy, config,
1✔
83
                                                        attribute, default)
84

85
        # Loop this list again to have output combined
86
        for attribute, _ in attributes:
1✔
87
            if attribute in config:
1✔
88
                logger.info("Strategy using %s: %s", attribute, config[attribute])
1✔
89

90
        StrategyResolver._normalize_attributes(strategy)
1✔
91

92
        StrategyResolver._strategy_sanity_validations(strategy)
1✔
93
        return strategy
1✔
94

95
    @staticmethod
1✔
96
    def _override_attribute_helper(strategy, config: Config, attribute: str, default: Any):
1✔
97
        """
98
        Override attributes in the strategy.
99
        Prevalence:
100
        - Configuration
101
        - Strategy
102
        - default (if not None)
103
        """
104
        if (attribute in config
1✔
105
                and not isinstance(getattr(type(strategy), attribute, None), property)):
106
            # Ensure Properties are not overwritten
107
            setattr(strategy, attribute, config[attribute])
1✔
108
            logger.info("Override strategy '%s' with value in config file: %s.",
1✔
109
                        attribute, config[attribute])
110
        elif hasattr(strategy, attribute):
1✔
111
            val = getattr(strategy, attribute)
1✔
112
            # None's cannot exist in the config, so do not copy them
113
            if val is not None:
1✔
114
                # max_open_trades set to -1 in the strategy will be copied as infinity in the config
115
                if attribute == 'max_open_trades' and val == -1:
1✔
116
                    config[attribute] = float('inf')
1✔
117
                else:
118
                    config[attribute] = val
1✔
119
        # Explicitly check for None here as other "falsy" values are possible
120
        elif default is not None:
1✔
121
            setattr(strategy, attribute, default)
1✔
122
            config[attribute] = default
1✔
123

124
    @staticmethod
1✔
125
    def _normalize_attributes(strategy: IStrategy) -> IStrategy:
1✔
126
        """
127
        Normalize attributes to have the correct type.
128
        """
129
        # Sort and apply type conversions
130
        if hasattr(strategy, 'minimal_roi'):
1✔
131
            strategy.minimal_roi = dict(sorted(
1✔
132
                {int(key): value for (key, value) in strategy.minimal_roi.items()}.items(),
133
                key=lambda t: t[0]))
134
        if hasattr(strategy, 'stoploss'):
1✔
135
            strategy.stoploss = float(strategy.stoploss)
1✔
136
        if hasattr(strategy, 'max_open_trades') and strategy.max_open_trades < 0:
1✔
137
            strategy.max_open_trades = float('inf')
1✔
138
        return strategy
1✔
139

140
    @staticmethod
1✔
141
    def _strategy_sanity_validations(strategy: IStrategy):
1✔
142
        # Ensure necessary migrations are performed first.
143
        validate_migrated_strategy_settings(strategy.config)
1✔
144

145
        if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES):
1✔
146
            raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
1✔
147
                              f"Order-types mapping is incomplete.")
148
        if not all(k in strategy.order_time_in_force for k in REQUIRED_ORDERTIF):
1✔
149
            raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
1✔
150
                              f"Order-time-in-force mapping is incomplete.")
151
        trading_mode = strategy.config.get('trading_mode', TradingMode.SPOT)
1✔
152

153
        if (strategy.can_short and trading_mode == TradingMode.SPOT):
1✔
154
            raise ImportError(
1✔
155
                "Short strategies cannot run in spot markets. Please make sure that this "
156
                "is the correct strategy and that your trading mode configuration is correct. "
157
                "You can run this strategy in spot markets by setting `can_short=False`"
158
                " in your strategy. Please note that short signals will be ignored in that case."
159
                )
160

161
    @staticmethod
1✔
162
    def validate_strategy(strategy: IStrategy) -> IStrategy:
1✔
163
        if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
1✔
164
            # Require new method
165
            warn_deprecated_setting(strategy, 'sell_profit_only', 'exit_profit_only', True)
1✔
166
            warn_deprecated_setting(strategy, 'sell_profit_offset', 'exit_profit_offset', True)
1✔
167
            warn_deprecated_setting(strategy, 'use_sell_signal', 'use_exit_signal', True)
1✔
168
            warn_deprecated_setting(strategy, 'ignore_roi_if_buy_signal',
1✔
169
                                    'ignore_roi_if_entry_signal', True)
170

171
            if not check_override(strategy, IStrategy, 'populate_entry_trend'):
1✔
172
                raise OperationalException("`populate_entry_trend` must be implemented.")
1✔
173
            if not check_override(strategy, IStrategy, 'populate_exit_trend'):
1✔
174
                raise OperationalException("`populate_exit_trend` must be implemented.")
1✔
175
            if check_override(strategy, IStrategy, 'check_buy_timeout'):
1✔
176
                raise OperationalException("Please migrate your implementation "
1✔
177
                                           "of `check_buy_timeout` to `check_entry_timeout`.")
178
            if check_override(strategy, IStrategy, 'check_sell_timeout'):
1✔
179
                raise OperationalException("Please migrate your implementation "
1✔
180
                                           "of `check_sell_timeout` to `check_exit_timeout`.")
181

182
            if check_override(strategy, IStrategy, 'custom_sell'):
1✔
183
                raise OperationalException(
1✔
184
                    "Please migrate your implementation of `custom_sell` to `custom_exit`.")
185

186
        else:
187
            # TODO: Implementing one of the following methods should show a deprecation warning
188
            #  buy_trend and sell_trend, custom_sell
189
            warn_deprecated_setting(strategy, 'sell_profit_only', 'exit_profit_only')
1✔
190
            warn_deprecated_setting(strategy, 'sell_profit_offset', 'exit_profit_offset')
1✔
191
            warn_deprecated_setting(strategy, 'use_sell_signal', 'use_exit_signal')
1✔
192
            warn_deprecated_setting(strategy, 'ignore_roi_if_buy_signal',
1✔
193
                                    'ignore_roi_if_entry_signal')
194

195
            if (
1✔
196
                not check_override(strategy, IStrategy, 'populate_buy_trend')
197
                and not check_override(strategy, IStrategy, 'populate_entry_trend')
198
            ):
199
                raise OperationalException(
1✔
200
                    "`populate_entry_trend` or `populate_buy_trend` must be implemented.")
201
            if (
1✔
202
                not check_override(strategy, IStrategy, 'populate_sell_trend')
203
                and not check_override(strategy, IStrategy, 'populate_exit_trend')
204
            ):
205
                raise OperationalException(
1✔
206
                    "`populate_exit_trend` or `populate_sell_trend` must be implemented.")
207

208
            _populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
1✔
209
            _buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
1✔
210
            _sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
1✔
211
            if any(x == 2 for x in [
1✔
212
                _populate_fun_len,
213
                _buy_fun_len,
214
                _sell_fun_len
215
            ]):
216
                raise OperationalException(
1✔
217
                    "Strategy Interface v1 is no longer supported. "
218
                    "Please update your strategy to implement "
219
                    "`populate_indicators`, `populate_entry_trend` and `populate_exit_trend` "
220
                    "with the metadata argument. ")
221

222
        has_after_fill = ('after_fill' in getfullargspec(strategy.custom_stoploss).args
1✔
223
                          and check_override(strategy, IStrategy, 'custom_stoploss'))
224
        if has_after_fill:
1✔
225
            strategy._ft_stop_uses_after_fill = True
×
226

227
        return strategy
1✔
228

229
    @staticmethod
1✔
230
    def _load_strategy(strategy_name: str,
1✔
231
                       config: Config, extra_dir: Optional[str] = None) -> IStrategy:
232
        """
233
        Search and loads the specified strategy.
234
        :param strategy_name: name of the module to import
235
        :param config: configuration for the strategy
236
        :param extra_dir: additional directory to search for the given strategy
237
        :return: Strategy instance or None
238
        """
239
        if config.get('recursive_strategy_search', False):
1✔
240
            extra_dirs: List[str] = [
×
241
                path[0] for path in walk(f"{config['user_data_dir']}/{USERPATH_STRATEGIES}")
242
            ]  # sub-directories
243
        else:
244
            extra_dirs = []
1✔
245

246
        if extra_dir:
1✔
247
            extra_dirs.append(extra_dir)
1✔
248

249
        abs_paths = StrategyResolver.build_search_paths(config,
1✔
250
                                                        user_subdir=USERPATH_STRATEGIES,
251
                                                        extra_dirs=extra_dirs)
252

253
        if ":" in strategy_name:
1✔
254
            logger.info("loading base64 encoded strategy")
1✔
255
            strat = strategy_name.split(":")
1✔
256

257
            if len(strat) == 2:
1✔
258
                temp = Path(tempfile.mkdtemp("freq", "strategy"))
1✔
259
                name = strat[0] + ".py"
1✔
260

261
                temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8'))
1✔
262
                temp.joinpath("__init__.py").touch()
1✔
263

264
                strategy_name = strat[0]
1✔
265

266
                # register temp path with the bot
267
                abs_paths.insert(0, temp.resolve())
1✔
268

269
        strategy = StrategyResolver._load_object(
1✔
270
            paths=abs_paths,
271
            object_name=strategy_name,
272
            add_source=True,
273
            kwargs={'config': config},
274
        )
275

276
        if strategy:
1✔
277

278
            return StrategyResolver.validate_strategy(strategy)
1✔
279

280
        raise OperationalException(
1✔
281
            f"Impossible to load Strategy '{strategy_name}'. This class does not exist "
282
            "or contains Python code errors."
283
        )
284

285

286
def warn_deprecated_setting(strategy: IStrategy, old: str, new: str, error=False):
1✔
287
    if hasattr(strategy, old):
1✔
288
        errormsg = f"DEPRECATED: Using '{old}' moved to '{new}'."
1✔
289
        if error:
1✔
290
            raise OperationalException(errormsg)
1✔
291
        logger.warning(errormsg)
1✔
292
        setattr(strategy, new, getattr(strategy, f'{old}'))
1✔
293

294

295
def check_override(object, parentclass, attribute):
1✔
296
    """
297
    Checks if a object overrides the parent class attribute.
298
    :returns: True if the object is overridden.
299
    """
300
    return getattr(type(object), attribute) != getattr(parentclass, attribute)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc