Merge branch 'develop' into feat/short
This commit is contained in:
@@ -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
|
||||
|
Reference in New Issue
Block a user