Merge branch 'develop' into feat/short

This commit is contained in:
Matthias
2022-03-22 20:28:38 +01:00
32 changed files with 127 additions and 229 deletions

View File

@@ -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:

View File

@@ -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`',

View File

@@ -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.")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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']

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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

View File

@@ -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