Merge branch 'develop' into feat/short
This commit is contained in:
commit
b1e3ead88b
@ -26,7 +26,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
@ -63,7 +63,7 @@ optional arguments:
|
||||
`30m`, `1h`, `1d`).
|
||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||
Provide a space-separated list of strategies to
|
||||
backtest. Please note that ticker-interval needs to be
|
||||
backtest. Please note that timeframe needs to be
|
||||
set either in config or via command line. When using
|
||||
this together with `--export trades`, the strategy-
|
||||
name is injected into the filename (so `backtest-
|
||||
|
@ -24,6 +24,10 @@ Please refer to [pairlists](plugins.md#pairlists-and-pairlist-handlers) instead.
|
||||
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
|
||||
Removed in 2019-7-dev (develop branch) and in freqtrade 2019.8.
|
||||
|
||||
### `ticker_interval` (now `timeframe`)
|
||||
|
||||
Support for `ticker_interval` terminology was deprecated in 2020.6 in favor of `timeframe` - and compatibility code was removed in 2022.3.
|
||||
|
||||
### Allow running multiple pairlists in sequence
|
||||
|
||||
The former `"pairlist"` section in the configuration has been removed, and is replaced by `"pairlists"` - being a list to specify a sequence of pairlists.
|
||||
|
@ -222,7 +222,7 @@ usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
|
@ -55,7 +55,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
|
@ -65,7 +65,7 @@ optional arguments:
|
||||
_today.json`
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--no-trades Skip using trades from backtesting file and DB.
|
||||
|
||||
@ -334,7 +334,7 @@ optional arguments:
|
||||
--trade-source {DB,file}
|
||||
Specify the source for trades (Can be DB or file
|
||||
(backtest file)) Default: file
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--auto-open Automatically open generated plot.
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
mkdocs==1.2.3
|
||||
mkdocs-material==8.2.5
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==9.2
|
||||
pymdown-extensions==9.3
|
||||
|
@ -146,7 +146,7 @@ def version(self) -> str:
|
||||
|
||||
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
||||
|
||||
``` python
|
||||
``` python title="user_data/strategies/myawesomestrategy.py"
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
...
|
||||
stoploss = 0.13
|
||||
@ -155,6 +155,10 @@ class MyAwesomeStrategy(IStrategy):
|
||||
# should be in any custom strategy...
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
``` python title="user_data/strategies/MyAwesomeStrategy2.py"
|
||||
from myawesomestrategy import MyAwesomeStrategy
|
||||
class MyAwesomeStrategy2(MyAwesomeStrategy):
|
||||
# Override something
|
||||
stoploss = 0.08
|
||||
@ -163,16 +167,7 @@ class MyAwesomeStrategy2(MyAwesomeStrategy):
|
||||
|
||||
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
|
||||
|
||||
!!! Note "Parent-strategy in different files"
|
||||
If you have the parent-strategy in a different file, you'll need to add the following to the top of your "child"-file to ensure proper loading, otherwise freqtrade may not be able to load the parent strategy correctly.
|
||||
|
||||
``` python
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.append(str(Path(__file__).parent))
|
||||
|
||||
from myawesomestrategy import MyAwesomeStrategy
|
||||
```
|
||||
While keeping the subclass in the same file is technically possible, it can lead to some problems with hyperopt parameter files, we therefore recommend to use separate strategy files, and import the parent strategy as shown above.
|
||||
|
||||
## Embedding Strategies
|
||||
|
||||
|
@ -274,15 +274,16 @@ Starting capital is either taken from the `available_capital` setting, or calcul
|
||||
|
||||
### /forcesell <trade_id>
|
||||
|
||||
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
||||
> **BINANCE:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
||||
|
||||
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
|
||||
|
||||
`/forcebuy <pair> [rate]` is also supported for longs but should be considered deprecated.
|
||||
|
||||
> **BITTREX:** Long ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
||||
> **BINANCE:** Long ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
||||
|
||||
Omitting the pair will open a query asking for the pair to trade (based on the current whitelist).
|
||||
Trades crated through `/forceentry` will have the buy-tag of `forceentry`.
|
||||
|
||||
![Telegram force-buy screenshot](assets/telegram_forcebuy.png)
|
||||
|
||||
|
@ -1,27 +1,14 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = 'develop'
|
||||
|
||||
if __version__ == 'develop':
|
||||
|
||||
if 'dev' in __version__:
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
__version__ = 'develop-' + subprocess.check_output(
|
||||
__version__ = __version__ + '-' + subprocess.check_output(
|
||||
['git', 'log', '--format="%h"', '-n 1'],
|
||||
stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||
|
||||
# from datetime import datetime
|
||||
# last_release = subprocess.check_output(
|
||||
# ['git', 'tag']
|
||||
# ).decode('utf-8').split()[-1].split(".")
|
||||
# # Releases are in the format "2020.1" - we increment the latest version for dev.
|
||||
# prefix = f"{last_release[0]}.{int(last_release[1]) + 1}"
|
||||
# dev_version = int(datetime.now().timestamp() // 1000)
|
||||
# __version__ = f"{prefix}.dev{dev_version}"
|
||||
|
||||
# subprocess.check_output(
|
||||
# ['git', 'log', '--format="%h"', '-n 1'],
|
||||
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||
except Exception: # pragma: no cover
|
||||
# git not available, ignore
|
||||
try:
|
||||
|
@ -118,7 +118,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
# Optimize common
|
||||
"timeframe": Arg(
|
||||
'-i', '--timeframe', '--ticker-interval',
|
||||
'-i', '--timeframe',
|
||||
help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||
),
|
||||
"timerange": Arg(
|
||||
@ -170,7 +170,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"strategy_list": Arg(
|
||||
'--strategy-list',
|
||||
help='Provide a space-separated list of strategies to backtest. '
|
||||
'Please note that ticker-interval needs to be set either in config '
|
||||
'Please note that timeframe needs to be set either in config '
|
||||
'or via command line. When using this together with `--export trades`, '
|
||||
'the strategy-name is injected into the filename '
|
||||
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
|
||||
|
@ -101,16 +101,11 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
"from the edge configuration."
|
||||
)
|
||||
if 'ticker_interval' in config:
|
||||
logger.warning(
|
||||
"DEPRECATED: "
|
||||
|
||||
raise OperationalException(
|
||||
"DEPRECATED: 'ticker_interval' detected. "
|
||||
"Please use 'timeframe' instead of 'ticker_interval."
|
||||
)
|
||||
if 'timeframe' in config:
|
||||
raise OperationalException(
|
||||
"Both 'timeframe' and 'ticker_interval' detected."
|
||||
"Please remove 'ticker_interval' from your configuration to continue operating."
|
||||
)
|
||||
config['timeframe'] = config['ticker_interval']
|
||||
|
||||
if 'protections' in config:
|
||||
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
||||
|
@ -1641,14 +1641,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
|
||||
# Try update amount (binance-fix)
|
||||
try:
|
||||
new_amount = self.get_real_amount(trade, order)
|
||||
new_amount = self.get_real_amount(trade, order, order_obj)
|
||||
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
|
||||
abs_tol=constants.MATH_CLOSE_PREC):
|
||||
order_obj.ft_fee_base = trade.amount - new_amount
|
||||
except DependencyException as exception:
|
||||
logger.warning("Could not update trade amount: %s", exception)
|
||||
|
||||
def get_real_amount(self, trade: Trade, order: Dict) -> float:
|
||||
def get_real_amount(self, trade: Trade, order: Dict, order_obj: Order) -> float:
|
||||
"""
|
||||
Detect and update trade fee.
|
||||
Calls trade.update_fee() upon correct detection.
|
||||
@ -1666,7 +1666,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# use fee from order-dict if possible
|
||||
if self.exchange.order_has_fee(order):
|
||||
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
|
||||
logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
|
||||
logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
|
||||
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
|
||||
if fee_rate is None or fee_rate < 0.02:
|
||||
# Reject all fees that report as > 2%.
|
||||
@ -1678,17 +1678,18 @@ class FreqtradeBot(LoggingMixin):
|
||||
return self.apply_fee_conditional(trade, trade_base_currency,
|
||||
amount=order_amount, fee_abs=fee_cost)
|
||||
return order_amount
|
||||
return self.fee_detection_from_trades(trade, order, order_amount, order.get('trades', []))
|
||||
return self.fee_detection_from_trades(
|
||||
trade, order, order_obj, order_amount, order.get('trades', []))
|
||||
|
||||
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float,
|
||||
trades: List) -> float:
|
||||
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_obj: Order,
|
||||
order_amount: float, trades: List) -> float:
|
||||
"""
|
||||
fee-detection fallback to Trades.
|
||||
Either uses provided trades list or the result of fetch_my_trades to get correct fee.
|
||||
"""
|
||||
if not trades:
|
||||
trades = self.exchange.get_trades_for_order(
|
||||
self.exchange.get_order_id_conditional(order), trade.pair, trade.open_date)
|
||||
self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date)
|
||||
|
||||
if len(trades) == 0:
|
||||
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
|
||||
|
@ -90,7 +90,7 @@ class Backtesting:
|
||||
validate_config_consistency(self.config)
|
||||
|
||||
if "timeframe" not in self.config:
|
||||
raise OperationalException("Timeframe (ticker interval) needs to be set in either "
|
||||
raise OperationalException("Timeframe needs to be set in either "
|
||||
"configuration or as cli argument `--timeframe 5m`")
|
||||
self.timeframe = str(self.config.get('timeframe'))
|
||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||
|
@ -29,15 +29,13 @@ class IHyperOpt(ABC):
|
||||
Class attributes you can use:
|
||||
timeframe -> int: value of the timeframe to use for the strategy
|
||||
"""
|
||||
ticker_interval: str # DEPRECATED
|
||||
timeframe: str
|
||||
strategy: IStrategy
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
self.config = config
|
||||
|
||||
# Assign ticker_interval to be used in hyperopt
|
||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
||||
# Assign timeframe to be used in hyperopt
|
||||
IHyperOpt.timeframe = str(config['timeframe'])
|
||||
|
||||
def generate_estimator(self, dimensions: List[Dimension], **kwargs) -> EstimatorType:
|
||||
@ -192,7 +190,7 @@ class IHyperOpt(ABC):
|
||||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
||||
]
|
||||
|
||||
# This is needed for proper unpickling the class attribute ticker_interval
|
||||
# This is needed for proper unpickling the class attribute timeframe
|
||||
# which is set to the actual value by the resolver.
|
||||
# Why do I still need such shamanic mantras in modern python?
|
||||
def __getstate__(self):
|
||||
@ -202,5 +200,4 @@ class IHyperOpt(ABC):
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
IHyperOpt.ticker_interval = state['timeframe']
|
||||
IHyperOpt.timeframe = state['timeframe']
|
||||
|
@ -44,7 +44,6 @@ class HyperOptLossResolver(IResolver):
|
||||
extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
# Assign timeframe to be used in hyperopt
|
||||
hyperoptloss.__class__.ticker_interval = str(config['timeframe'])
|
||||
hyperoptloss.__class__.timeframe = str(config['timeframe'])
|
||||
|
||||
return hyperoptloss
|
||||
|
@ -6,6 +6,7 @@ This module load custom objects
|
||||
import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
|
||||
|
||||
@ -15,6 +16,22 @@ from freqtrade.exceptions import OperationalException
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PathModifier:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
|
||||
def __enter__(self):
|
||||
"""Inject path to allow importing with relative imports."""
|
||||
sys.path.insert(0, str(self.path))
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Undo insertion of local path."""
|
||||
str_path = str(self.path)
|
||||
if str_path in sys.path:
|
||||
sys.path.remove(str_path)
|
||||
|
||||
|
||||
class IResolver:
|
||||
"""
|
||||
This class contains all the logic to load custom classes
|
||||
@ -57,27 +74,32 @@ class IResolver:
|
||||
|
||||
# Generate spec based on absolute path
|
||||
# Pass object_name as first argument to have logging print a reasonable name.
|
||||
spec = importlib.util.spec_from_file_location(object_name or "", str(module_path))
|
||||
if not spec:
|
||||
return iter([None])
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||
except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err:
|
||||
# Catch errors in case a specific module is not installed
|
||||
logger.warning(f"Could not import {module_path} due to '{err}'")
|
||||
if enum_failed:
|
||||
with PathModifier(module_path.parent):
|
||||
module_name = module_path.stem or ""
|
||||
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
|
||||
if not spec:
|
||||
return iter([None])
|
||||
|
||||
valid_objects_gen = (
|
||||
(obj, inspect.getsource(module)) for
|
||||
name, obj in inspect.getmembers(
|
||||
module, inspect.isclass) if ((object_name is None or object_name == name)
|
||||
and issubclass(obj, cls.object_type)
|
||||
and obj is not cls.object_type)
|
||||
)
|
||||
return valid_objects_gen
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||
except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err:
|
||||
# Catch errors in case a specific module is not installed
|
||||
logger.warning(f"Could not import {module_path} due to '{err}'")
|
||||
if enum_failed:
|
||||
return iter([None])
|
||||
|
||||
valid_objects_gen = (
|
||||
(obj, inspect.getsource(module)) for
|
||||
name, obj in inspect.getmembers(
|
||||
module, inspect.isclass) if ((object_name is None or object_name == name)
|
||||
and issubclass(obj, cls.object_type)
|
||||
and obj is not cls.object_type
|
||||
and obj.__module__ == module_name
|
||||
)
|
||||
)
|
||||
# The __module__ check ensures we only use strategies that are defined in this folder.
|
||||
return valid_objects_gen
|
||||
|
||||
@classmethod
|
||||
def _search_object(cls, directory: Path, *, object_name: str, add_source: bool = False
|
||||
|
@ -47,14 +47,6 @@ class StrategyResolver(IResolver):
|
||||
strategy_name, config=config,
|
||||
extra_dir=config.get('strategy_path'))
|
||||
|
||||
if hasattr(strategy, 'ticker_interval') and not hasattr(strategy, 'timeframe'):
|
||||
# Assign ticker_interval to timeframe to keep compatibility
|
||||
if 'timeframe' not in config:
|
||||
logger.warning(
|
||||
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
|
||||
)
|
||||
strategy.timeframe = strategy.ticker_interval
|
||||
|
||||
if strategy._ft_params_from_file:
|
||||
# Set parameters from Hyperopt results file
|
||||
params = strategy._ft_params_from_file
|
||||
@ -147,10 +139,6 @@ class StrategyResolver(IResolver):
|
||||
"""
|
||||
Normalize attributes to have the correct type.
|
||||
"""
|
||||
# Assign deprecated variable - to not break users code relying on this.
|
||||
if hasattr(strategy, 'timeframe'):
|
||||
strategy.ticker_interval = strategy.timeframe
|
||||
|
||||
# Sort and apply type conversions
|
||||
if hasattr(strategy, 'minimal_roi'):
|
||||
strategy.minimal_roi = dict(sorted(
|
||||
|
@ -141,7 +141,7 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
||||
def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
stake_amount = payload.stakeamount if payload.stakeamount else None
|
||||
entry_tag = payload.entry_tag if payload.entry_tag else None
|
||||
entry_tag = payload.entry_tag if payload.entry_tag else 'forceentry'
|
||||
|
||||
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
|
||||
order_type=ordertype, stake_amount=stake_amount,
|
||||
|
@ -747,7 +747,7 @@ class RPC:
|
||||
order_type: Optional[str] = None,
|
||||
order_side: SignalDirection = SignalDirection.LONG,
|
||||
stake_amount: Optional[float] = None,
|
||||
enter_tag: Optional[str] = None) -> Optional[Trade]:
|
||||
enter_tag: Optional[str] = 'forceentry') -> Optional[Trade]:
|
||||
"""
|
||||
Handler for forcebuy <asset> <price>
|
||||
Buys a pair trade at the given or current price
|
||||
|
@ -56,7 +56,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
Attributes you can use:
|
||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||
stoploss -> float: optimal stoploss designed for the strategy
|
||||
timeframe -> str: value of the timeframe (ticker interval) to use with the strategy
|
||||
timeframe -> str: value of the timeframe to use with the strategy
|
||||
"""
|
||||
# Strategy interface version
|
||||
# Default to version 2
|
||||
@ -86,7 +86,6 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
can_short: bool = False
|
||||
|
||||
# associated timeframe
|
||||
ticker_interval: str # DEPRECATED
|
||||
timeframe: str
|
||||
|
||||
# Optional order types
|
||||
|
@ -6,8 +6,8 @@
|
||||
coveralls==3.3.1
|
||||
flake8==4.0.1
|
||||
flake8-tidy-imports==4.6.0
|
||||
mypy==0.940
|
||||
pytest==7.1.0
|
||||
mypy==0.941
|
||||
pytest==7.1.1
|
||||
pytest-asyncio==0.18.2
|
||||
pytest-cov==3.0.0
|
||||
pytest-mock==3.7.0
|
||||
@ -22,8 +22,8 @@ nbconvert==6.4.4
|
||||
# mypy types
|
||||
types-cachetools==5.0.0
|
||||
types-filelock==3.2.5
|
||||
types-requests==2.27.12
|
||||
types-tabulate==0.8.5
|
||||
types-requests==2.27.14
|
||||
types-tabulate==0.8.6
|
||||
|
||||
# Extensions to datetime library
|
||||
types-python-dateutil==2.8.9
|
||||
types-python-dateutil==2.8.10
|
@ -2,16 +2,16 @@ numpy==1.22.3
|
||||
pandas==1.4.1
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==1.76.5
|
||||
ccxt==1.76.65
|
||||
# Pin cryptography for now due to rust build errors with piwheels
|
||||
cryptography==36.0.1
|
||||
cryptography==36.0.2
|
||||
aiohttp==3.8.1
|
||||
SQLAlchemy==1.4.32
|
||||
python-telegram-bot==13.11
|
||||
arrow==1.2.2
|
||||
cachetools==4.2.2
|
||||
requests==2.27.1
|
||||
urllib3==1.26.8
|
||||
urllib3==1.26.9
|
||||
jsonschema==4.4.0
|
||||
TA-Lib==0.4.24
|
||||
technical==1.3.0
|
||||
|
@ -318,17 +318,15 @@ def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
|
||||
patch_exchange(mocker)
|
||||
del default_conf['timeframe']
|
||||
default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY,
|
||||
'SampleStrategy']
|
||||
# TODO: This refers to the sampleStrategy in user_data if it exists...
|
||||
'HyperoptableStrategy']
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||
with pytest.raises(OperationalException):
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Timeframe needs to be set in either configuration"):
|
||||
Backtesting(default_conf)
|
||||
log_has("Ticker-interval needs to be set in either configuration "
|
||||
"or as cli argument `--ticker-interval 5m`", caplog)
|
||||
|
||||
|
||||
def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
|
||||
def test_data_with_fee(default_conf, mocker) -> None:
|
||||
patch_exchange(mocker)
|
||||
default_conf['fee'] = 0.1234
|
||||
|
||||
|
@ -6,7 +6,7 @@ from unittest.mock import MagicMock
|
||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.optimize.edge_cli import EdgeCli
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, patch_exchange,
|
||||
patched_configuration_load_config_file)
|
||||
|
||||
|
||||
@ -30,7 +30,6 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
|
||||
assert 'datadir' in config
|
||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||
assert 'timeframe' in config
|
||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||
|
||||
assert 'timerange' not in config
|
||||
assert 'stoploss_range' not in config
|
||||
|
@ -63,7 +63,6 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
|
||||
assert 'datadir' in config
|
||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||
assert 'timeframe' in config
|
||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||
|
||||
assert 'position_stacking' not in config
|
||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||
|
@ -1219,6 +1219,7 @@ def test_rpc_forceentry(mocker, default_conf, ticker, fee, limit_buy_order_open)
|
||||
pair = 'LTC/BTC'
|
||||
trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05)
|
||||
assert trade.stake_amount == 0.05
|
||||
assert trade.buy_tag == 'forceentry'
|
||||
|
||||
# Test not buying
|
||||
pair = 'XRP/BTC'
|
||||
|
@ -1,14 +1,13 @@
|
||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from strategy_test_v2 import StrategyTestV2
|
||||
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
|
||||
RealParameter)
|
||||
from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter
|
||||
|
||||
|
||||
class HyperoptableStrategy(IStrategy):
|
||||
class HyperoptableStrategy(StrategyTestV2):
|
||||
"""
|
||||
Default Strategy provided by freqtrade bot.
|
||||
Please do not modify this strategy, it's intended for internal use only.
|
||||
@ -16,38 +15,6 @@ class HyperoptableStrategy(IStrategy):
|
||||
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||
for samples and inspiration.
|
||||
"""
|
||||
INTERFACE_VERSION = 2
|
||||
|
||||
# Minimal ROI designed for the strategy
|
||||
minimal_roi = {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
}
|
||||
|
||||
# Optimal stoploss designed for the strategy
|
||||
stoploss = -0.10
|
||||
|
||||
# Optimal ticker interval for the strategy
|
||||
timeframe = '5m'
|
||||
|
||||
# Optional order type mapping
|
||||
order_types = {
|
||||
'entry': 'limit',
|
||||
'exit': 'limit',
|
||||
'stoploss': 'limit',
|
||||
'stoploss_on_exchange': False
|
||||
}
|
||||
|
||||
# Number of candles the strategy requires before producing valid signals
|
||||
startup_candle_count: int = 20
|
||||
|
||||
# Optional time in force for orders
|
||||
order_time_in_force = {
|
||||
'entry': 'gtc',
|
||||
'exit': 'gtc',
|
||||
}
|
||||
|
||||
buy_params = {
|
||||
'buy_rsi': 35,
|
||||
@ -91,55 +58,6 @@ class HyperoptableStrategy(IStrategy):
|
||||
"""
|
||||
return []
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
:param dataframe: Dataframe with data from the exchange
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
# ------------------------------------
|
||||
|
||||
# ADX
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
|
||||
# Minus Directional Indicator / Movement
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# Plus Directional Indicator / Movement
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
|
||||
# RSI
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
|
@ -31,9 +31,7 @@ class TestStrategyLegacyV1(IStrategy):
|
||||
# This attribute will be overridden if the config file contains "stoploss"
|
||||
stoploss = -0.10
|
||||
|
||||
# Optimal timeframe for the strategy
|
||||
# Keep the legacy value here to test compatibility
|
||||
ticker_interval = '5m'
|
||||
timeframe = '5m'
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
|
@ -113,7 +113,6 @@ def test_strategy_pre_v3(result, default_conf, strategy_name):
|
||||
assert default_conf['stoploss'] == -0.10
|
||||
|
||||
assert strategy.timeframe == '5m'
|
||||
assert strategy.ticker_interval == '5m'
|
||||
assert default_conf['timeframe'] == '5m'
|
||||
|
||||
df_indicators = strategy.advise_indicators(result, metadata=metadata)
|
||||
@ -440,7 +439,6 @@ def test_call_deprecated_function(result, default_conf, caplog):
|
||||
assert strategy._sell_fun_len == 2
|
||||
assert strategy.INTERFACE_VERSION == 1
|
||||
assert strategy.timeframe == '5m'
|
||||
assert strategy.ticker_interval == '5m'
|
||||
|
||||
indicator_df = strategy.advise_indicators(result, metadata=metadata)
|
||||
assert isinstance(indicator_df, DataFrame)
|
||||
@ -454,9 +452,6 @@ def test_call_deprecated_function(result, default_conf, caplog):
|
||||
assert isinstance(exitdf, DataFrame)
|
||||
assert 'exit_long' in exitdf
|
||||
|
||||
assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.",
|
||||
caplog)
|
||||
|
||||
|
||||
def test_strategy_interface_versioning(result, default_conf):
|
||||
default_conf.update({'strategy': 'StrategyTestV2'})
|
||||
|
@ -112,17 +112,17 @@ def test_parse_args_strategy_path_invalid() -> None:
|
||||
|
||||
def test_parse_args_backtesting_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['backtesting --ticker-interval']).get_parsed_arg()
|
||||
Arguments(['backtesting --timeframe']).get_parsed_arg()
|
||||
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['backtesting --ticker-interval', 'abc']).get_parsed_arg()
|
||||
Arguments(['backtesting --timeframe', 'abc']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_backtesting_custom() -> None:
|
||||
args = [
|
||||
'backtesting',
|
||||
'-c', 'test_conf.json',
|
||||
'--ticker-interval', '1m',
|
||||
'--timeframe', '1m',
|
||||
'--strategy-list',
|
||||
CURRENT_TEST_STRATEGY,
|
||||
'SampleStrategy'
|
||||
|
@ -444,7 +444,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
'--strategy', CURRENT_TEST_STRATEGY,
|
||||
'--datadir', '/foo/bar',
|
||||
'--userdir', "/tmp/freqtrade",
|
||||
'--ticker-interval', '1m',
|
||||
'--timeframe', '1m',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions',
|
||||
'--timerange', ':100',
|
||||
@ -495,7 +495,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
|
||||
arglist = [
|
||||
'backtesting',
|
||||
'--config', 'config.json',
|
||||
'--ticker-interval', '1m',
|
||||
'--timeframe', '1m',
|
||||
'--export', 'trades',
|
||||
'--strategy-list',
|
||||
CURRENT_TEST_STRATEGY,
|
||||
@ -1381,22 +1381,14 @@ def test_process_removed_setting(mocker, default_conf, caplog):
|
||||
def test_process_deprecated_ticker_interval(default_conf, caplog):
|
||||
message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval."
|
||||
config = deepcopy(default_conf)
|
||||
|
||||
process_temporary_deprecated_settings(config)
|
||||
assert not log_has(message, caplog)
|
||||
|
||||
del config['timeframe']
|
||||
config['ticker_interval'] = '15m'
|
||||
process_temporary_deprecated_settings(config)
|
||||
assert log_has(message, caplog)
|
||||
assert config['ticker_interval'] == '15m'
|
||||
|
||||
config = deepcopy(default_conf)
|
||||
# Have both timeframe and ticker interval in config
|
||||
# Can also happen when using ticker_interval in configuration, and --timeframe as cli argument
|
||||
config['timeframe'] = '5m'
|
||||
config['ticker_interval'] = '4h'
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Both 'timeframe' and 'ticker_interval' detected."):
|
||||
match=r"DEPRECATED: 'ticker_interval' detected. Please use.*"):
|
||||
process_temporary_deprecated_settings(config)
|
||||
|
||||
|
||||
|
@ -4107,8 +4107,9 @@ def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fe
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
caplog.clear()
|
||||
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||
# Amount is reduced by "fee"
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount - (amount * 0.001)
|
||||
assert log_has(
|
||||
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,'
|
||||
' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
|
||||
@ -4134,8 +4135,9 @@ def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_ord
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
walletmock.reset_mock()
|
||||
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||
# Amount is kept as is
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount
|
||||
assert walletmock.call_count == 1
|
||||
assert log_has_re(r'Fee amount for Trade.* was in base currency '
|
||||
'- Eating Fee 0.008 into dust', caplog)
|
||||
@ -4156,8 +4158,9 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||
# Amount is reduced by "fee"
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount
|
||||
assert log_has(
|
||||
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||
'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) failed: '
|
||||
@ -4213,7 +4216,8 @@ def test_get_real_amount(
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError)
|
||||
|
||||
caplog.clear()
|
||||
assert freqtrade.get_real_amount(trade, buy_order) == amount - fee_reduction_amount
|
||||
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||
assert freqtrade.get_real_amount(trade, buy_order, order_obj) == amount - fee_reduction_amount
|
||||
|
||||
if expected_log:
|
||||
assert log_has(expected_log, caplog)
|
||||
@ -4260,7 +4264,8 @@ def test_get_real_amount_multi(
|
||||
|
||||
# Amount is reduced by "fee"
|
||||
expected_amount = amount - (amount * fee_reduction_amount)
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == expected_amount
|
||||
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == expected_amount
|
||||
assert log_has(
|
||||
(
|
||||
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||
@ -4296,8 +4301,9 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||
# Amount does not change
|
||||
assert freqtrade.get_real_amount(trade, limit_buy_order_usdt) == amount
|
||||
assert freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj) == amount
|
||||
|
||||
|
||||
def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_doublefee,
|
||||
@ -4319,7 +4325,8 @@ def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_dou
|
||||
|
||||
# Amount does not change
|
||||
assert trade.fee_open == 0.0025
|
||||
assert freqtrade.get_real_amount(trade, market_buy_order_usdt_doublefee) == 30.0
|
||||
order_obj = Order.parse_from_ccxt_object(market_buy_order_usdt_doublefee, 'LTC/ETH', 'buy')
|
||||
assert freqtrade.get_real_amount(trade, market_buy_order_usdt_doublefee, order_obj) == 30.0
|
||||
assert tfo_mock.call_count == 0
|
||||
# Fetch fees from trades dict if available to get "proper" values
|
||||
assert round(trade.fee_open, 4) == 0.001
|
||||
@ -4343,9 +4350,10 @@ def test_get_real_amount_wrong_amount(default_conf_usdt, trades_for_order, buy_o
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||
# Amount does not change
|
||||
with pytest.raises(DependencyException, match=r"Half bought\? Amounts don't match"):
|
||||
freqtrade.get_real_amount(trade, limit_buy_order_usdt)
|
||||
freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj)
|
||||
|
||||
|
||||
def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_order, buy_order_fee,
|
||||
@ -4367,9 +4375,10 @@ def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_ord
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||
# Amount changes by fee amount.
|
||||
assert isclose(
|
||||
freqtrade.get_real_amount(trade, limit_buy_order_usdt),
|
||||
freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj),
|
||||
amount - (amount * 0.001),
|
||||
abs_tol=MATH_CLOSE_PREC,
|
||||
)
|
||||
@ -4393,7 +4402,8 @@ def test_get_real_amount_open_trade_usdt(default_conf_usdt, fee, mocker):
|
||||
'side': 'buy',
|
||||
}
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
assert freqtrade.get_real_amount(trade, order) == amount
|
||||
order_obj = Order.parse_from_ccxt_object(order, 'LTC/ETH', 'buy')
|
||||
assert freqtrade.get_real_amount(trade, order, order_obj) == amount
|
||||
|
||||
|
||||
@pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [
|
||||
|
Loading…
Reference in New Issue
Block a user