Merge branch 'develop' into safe_sell_amount

This commit is contained in:
Matthias
2019-12-28 09:35:45 +01:00
54 changed files with 745 additions and 605 deletions

View File

@@ -118,14 +118,14 @@ AVAILABLE_CLI_OPTIONS = {
help='Specify what timerange of data to use.',
),
"max_open_trades": Arg(
'--max_open_trades',
help='Specify max_open_trades to use.',
'--max-open-trades',
help='Override the value of the `max_open_trades` configuration setting.',
type=int,
metavar='INT',
),
"stake_amount": Arg(
'--stake_amount',
help='Specify stake_amount.',
'--stake-amount',
help='Override the value of the `stake_amount` configuration setting.',
type=float,
),
# Backtesting

View File

@@ -223,13 +223,13 @@ class Configuration:
logger.info('max_open_trades set to unlimited ...')
elif 'max_open_trades' in self.args and self.args["max_open_trades"]:
config.update({'max_open_trades': self.args["max_open_trades"]})
logger.info('Parameter --max_open_trades detected, '
logger.info('Parameter --max-open-trades detected, '
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
elif config['runmode'] in NON_UTIL_MODES:
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
self._args_to_config(config, argname='stake_amount',
logstring='Parameter --stake_amount detected, '
logstring='Parameter --stake-amount detected, '
'overriding stake_amount to: {} ...')
self._args_to_config(config, argname='fee',
@@ -403,7 +403,7 @@ class Configuration:
config['pairs'] = config.get('exchange', {}).get('pair_whitelist')
else:
# Fall back to /dl_path/pairs.json
pairs_file = Path(config['datadir']) / "pairs.json"
pairs_file = config['datadir'] / "pairs.json"
if pairs_file.exists():
with pairs_file.open('r') as f:
config['pairs'] = json_load(f)

View File

@@ -9,7 +9,7 @@ from freqtrade.constants import USER_DATA_FILES
logger = logging.getLogger(__name__)
def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str:
def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Path:
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
if not datadir:
@@ -20,7 +20,7 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str
if not folder.is_dir():
folder.mkdir(parents=True)
logger.info(f'Created data directory: {datadir}')
return str(folder)
return folder
def create_userdata_dir(directory: str, create_dir=False) -> Path:

View File

@@ -10,7 +10,7 @@ HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
UNLIMITED_STAKE_AMOUNT = 'unlimited'
DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05
REQUIRED_ORDERTIF = ['buy', 'sell']

View File

@@ -5,7 +5,6 @@ including Klines, tickers, historic data
Common Interface for bot and strategy to access data.
"""
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame
@@ -65,7 +64,7 @@ class DataProvider:
"""
return load_pair_history(pair=pair,
timeframe=timeframe or self._config['ticker_interval'],
datadir=Path(self._config['datadir'])
datadir=self._config['datadir']
)
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:

View File

@@ -1,7 +1,6 @@
# pragma pylint: disable=W0603
""" Edge positioning package """
import logging
from pathlib import Path
from typing import Any, Dict, NamedTuple
import arrow
@@ -96,7 +95,7 @@ class Edge:
if self._refresh_pairs:
history.refresh_data(
datadir=Path(self.config['datadir']),
datadir=self.config['datadir'],
pairs=pairs,
exchange=self.exchange,
timeframe=self.strategy.ticker_interval,
@@ -104,7 +103,7 @@ class Edge:
)
data = history.load_data(
datadir=Path(self.config['datadir']),
datadir=self.config['datadir'],
pairs=pairs,
timeframe=self.strategy.ticker_interval,
timerange=self._timerange,

View File

@@ -278,7 +278,15 @@ class Exchange:
raise OperationalException(
f'Pair {pair} is not available on {self.name}. '
f'Please remove {pair} from your whitelist.')
elif self.markets[pair].get('info', {}).get('IsRestricted', False):
# From ccxt Documentation:
# markets.info: An associative array of non-common market properties,
# including fees, rates, limits and other general market information.
# The internal info array is different for each particular market,
# its contents depend on the exchange.
# It can also be a string or similar ... so we need to verify that first.
elif (isinstance(self.markets[pair].get('info', None), dict)
and self.markets[pair].get('info', {}).get('IsRestricted', False)):
# Warn users about restricted pairs in whitelist.
# We cannot determine reliably if Users are affected.
logger.warning(f"Pair {pair} is restricted for some users on this exchange."
@@ -524,7 +532,7 @@ class Exchange:
raise OperationalException(e) from e
@retrier
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
def fetch_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self._cached_ticker.keys():
try:
if pair not in self._api.markets or not self._api.markets[pair].get('active'):

View File

@@ -55,12 +55,12 @@ class FreqtradeBot:
self.heartbeat_interval = self.config.get('internals', {}).get('heartbeat_interval', 60)
self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
# Check config consistency here since strategies can set certain options
validate_config_consistency(config)
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
persistence.init(self.config.get('db_url', None),
clean_open_orders=self.config.get('dry_run', False))
@@ -192,7 +192,7 @@ class FreqtradeBot:
else:
if not tick:
logger.info('Using Last Ask / Last Price')
ticker = self.exchange.get_ticker(pair)
ticker = self.exchange.fetch_ticker(pair)
else:
ticker = tick
if ticker['ask'] < ticker['last']:
@@ -570,7 +570,7 @@ class FreqtradeBot:
"""
Get sell rate - either using get-ticker bid or first bid based on orderbook
The orderbook portion is only used for rpc messaging, which would otherwise fail
for BitMex (has no bid/ask in get_ticker)
for BitMex (has no bid/ask in fetch_ticker)
or remain static in any other case since it's not updating.
:return: Bid rate
"""
@@ -582,7 +582,7 @@ class FreqtradeBot:
rate = order_book['bids'][0][0]
else:
rate = self.exchange.get_ticker(pair, refresh)['bid']
rate = self.exchange.fetch_ticker(pair, refresh)['bid']
return rate
def handle_trade(self, trade: Trade) -> bool:

View File

@@ -60,7 +60,7 @@ class Backtesting:
# Reset keys for backtesting
remove_credentials(self.config)
self.strategylist: List[IStrategy] = []
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
if config.get('fee'):
self.fee = config['fee']
@@ -75,12 +75,12 @@ class Backtesting:
for strat in list(self.config['strategy_list']):
stratconf = deepcopy(self.config)
stratconf['strategy'] = strat
self.strategylist.append(StrategyResolver(stratconf).strategy)
self.strategylist.append(StrategyResolver.load_strategy(stratconf))
validate_config_consistency(stratconf)
else:
# No strategy list specified, only one strategy
self.strategylist.append(StrategyResolver(self.config).strategy)
self.strategylist.append(StrategyResolver.load_strategy(self.config))
validate_config_consistency(self.config)
if "ticker_interval" not in self.config:
@@ -109,7 +109,7 @@ class Backtesting:
'timerange') is None else str(self.config.get('timerange')))
data = history.load_data(
datadir=Path(self.config['datadir']),
datadir=self.config['datadir'],
pairs=self.config['exchange']['pair_whitelist'],
timeframe=self.timeframe,
timerange=timerange,
@@ -183,9 +183,11 @@ class Backtesting:
Generate small table outlining Backtest results
"""
tabular_data = []
headers = ['Sell Reason', 'Count']
headers = ['Sell Reason', 'Count', 'Profit', 'Loss']
for reason, count in results['sell_reason'].value_counts().iteritems():
tabular_data.append([reason.value, count])
profit = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] >= 0)])
loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)])
tabular_data.append([reason.value, count, profit, loss])
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
def _generate_text_table_strategy(self, all_results: dict) -> str:

View File

@@ -34,7 +34,7 @@ class EdgeCli:
remove_credentials(self.config)
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
self.exchange = Exchange(self.config)
self.strategy = StrategyResolver(self.config).strategy
self.strategy = StrategyResolver.load_strategy(self.config)
validate_config_consistency(self.config)
@@ -42,11 +42,9 @@ class EdgeCli:
# Set refresh_pairs to false for edge-cli (it must be true for edge)
self.edge._refresh_pairs = False
self.timerange = TimeRange.parse_timerange(None if self.config.get(
self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
self.edge._timerange = self.timerange
def _generate_edge_table(self, results: dict) -> str:
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d')

View File

@@ -64,9 +64,9 @@ class Hyperopt:
self.backtesting = Backtesting(self.config)
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
self.trials_file = (self.config['user_data_dir'] /

View File

@@ -28,13 +28,13 @@ class PairListManager():
if 'method' not in pl:
logger.warning(f"No method in {pl}")
continue
pairl = PairListResolver(pl.get('method'),
exchange=exchange,
pairlistmanager=self,
config=config,
pairlistconfig=pl,
pairlist_pos=len(self._pairlists)
).pairlist
pairl = PairListResolver.load_pairlist(pl.get('method'),
exchange=exchange,
pairlistmanager=self,
config=config,
pairlistconfig=pl,
pairlist_pos=len(self._pairlists)
)
self._tickers_needed = pairl.needstickers or self._tickers_needed
self._pairlists.append(pairl)

View File

@@ -37,7 +37,7 @@ def init_plotscript(config):
timerange = TimeRange.parse_timerange(config.get("timerange"))
tickers = history.load_data(
datadir=Path(str(config.get("datadir"))),
datadir=config.get("datadir"),
pairs=pairs,
timeframe=config.get('ticker_interval', '5m'),
timerange=timerange,
@@ -340,7 +340,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
- Generate plot files
:return: None
"""
strategy = StrategyResolver(config).strategy
strategy = StrategyResolver.load_strategy(config)
plot_elements = init_plotscript(config)
trades = plot_elements['trades']

View File

@@ -15,9 +15,8 @@ class ExchangeResolver(IResolver):
This class contains all the logic to load a custom exchange class
"""
__slots__ = ['exchange']
def __init__(self, exchange_name: str, config: dict, validate: bool = True) -> None:
@staticmethod
def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange:
"""
Load the custom class from config parameter
:param config: configuration dictionary
@@ -25,17 +24,20 @@ class ExchangeResolver(IResolver):
# Map exchange name to avoid duplicate classes for identical exchanges
exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name)
exchange_name = exchange_name.title()
exchange = None
try:
self.exchange = self._load_exchange(exchange_name, kwargs={'config': config,
'validate': validate})
exchange = ExchangeResolver._load_exchange(exchange_name,
kwargs={'config': config,
'validate': validate})
except ImportError:
logger.info(
f"No {exchange_name} specific subclass found. Using the generic class instead.")
if not hasattr(self, "exchange"):
self.exchange = Exchange(config, validate=validate)
if not exchange:
exchange = Exchange(config, validate=validate)
return exchange
def _load_exchange(
self, exchange_name: str, kwargs: dict) -> Exchange:
@staticmethod
def _load_exchange(exchange_name: str, kwargs: dict) -> Exchange:
"""
Loads the specified exchange.
Only checks for exchanges exported in freqtrade.exchanges

View File

@@ -20,11 +20,11 @@ class HyperOptResolver(IResolver):
"""
This class contains all the logic to load custom hyperopt class
"""
__slots__ = ['hyperopt']
def __init__(self, config: Dict) -> None:
@staticmethod
def load_hyperopt(config: Dict) -> IHyperOpt:
"""
Load the custom class from config parameter
Load the custom hyperopt class from config parameter
:param config: configuration dictionary
"""
if not config.get('hyperopt'):
@@ -33,21 +33,23 @@ class HyperOptResolver(IResolver):
hyperopt_name = config['hyperopt']
self.hyperopt = self._load_hyperopt(hyperopt_name, config,
extra_dir=config.get('hyperopt_path'))
hyperopt = HyperOptResolver._load_hyperopt(hyperopt_name, config,
extra_dir=config.get('hyperopt_path'))
if not hasattr(self.hyperopt, 'populate_indicators'):
if not hasattr(hyperopt, 'populate_indicators'):
logger.warning("Hyperopt class does not provide populate_indicators() method. "
"Using populate_indicators from the strategy.")
if not hasattr(self.hyperopt, 'populate_buy_trend'):
if not hasattr(hyperopt, 'populate_buy_trend'):
logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
"Using populate_buy_trend from the strategy.")
if not hasattr(self.hyperopt, 'populate_sell_trend'):
if not hasattr(hyperopt, 'populate_sell_trend'):
logger.warning("Hyperopt class does not provide populate_sell_trend() method. "
"Using populate_sell_trend from the strategy.")
return hyperopt
@staticmethod
def _load_hyperopt(
self, hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt:
hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt:
"""
Search and loads the specified hyperopt.
:param hyperopt_name: name of the module to import
@@ -57,11 +59,12 @@ class HyperOptResolver(IResolver):
"""
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir)
abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=USERPATH_HYPEROPTS,
extra_dir=extra_dir)
hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt,
object_name=hyperopt_name, kwargs={'config': config})
hyperopt = IResolver._load_object(paths=abs_paths, object_type=IHyperOpt,
object_name=hyperopt_name, kwargs={'config': config})
if hyperopt:
return hyperopt
raise OperationalException(
@@ -74,9 +77,9 @@ class HyperOptLossResolver(IResolver):
"""
This class contains all the logic to load custom hyperopt loss class
"""
__slots__ = ['hyperoptloss']
def __init__(self, config: Dict) -> None:
@staticmethod
def load_hyperoptloss(config: Dict) -> IHyperOptLoss:
"""
Load the custom class from config parameter
:param config: configuration dictionary
@@ -86,20 +89,21 @@ class HyperOptLossResolver(IResolver):
# default hyperopt loss
hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
self.hyperoptloss = self._load_hyperoptloss(
hyperoptloss = HyperOptLossResolver._load_hyperoptloss(
hyperoptloss_name, config, extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt
self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'):
if not hasattr(hyperoptloss, 'hyperopt_loss_function'):
raise OperationalException(
f"Found HyperoptLoss class {hyperoptloss_name} does not "
"implement `hyperopt_loss_function`.")
return hyperoptloss
def _load_hyperoptloss(
self, hyper_loss_name: str, config: Dict,
extra_dir: Optional[str] = None) -> IHyperOptLoss:
@staticmethod
def _load_hyperoptloss(hyper_loss_name: str, config: Dict,
extra_dir: Optional[str] = None) -> IHyperOptLoss:
"""
Search and loads the specified hyperopt loss class.
:param hyper_loss_name: name of the module to import
@@ -109,11 +113,12 @@ class HyperOptLossResolver(IResolver):
"""
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir)
abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=USERPATH_HYPEROPTS,
extra_dir=extra_dir)
hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss,
object_name=hyper_loss_name)
hyperoptloss = IResolver._load_object(paths=abs_paths, object_type=IHyperOptLoss,
object_name=hyper_loss_name)
if hyperoptloss:
return hyperoptloss

View File

@@ -17,7 +17,8 @@ class IResolver:
This class contains all the logic to load custom classes
"""
def build_search_paths(self, config, current_path: Path, user_subdir: Optional[str] = None,
@staticmethod
def build_search_paths(config, current_path: Path, user_subdir: Optional[str] = None,
extra_dir: Optional[str] = None) -> List[Path]:
abs_paths: List[Path] = [current_path]

View File

@@ -18,23 +18,29 @@ class PairListResolver(IResolver):
This class contains all the logic to load custom PairList class
"""
__slots__ = ['pairlist']
def __init__(self, pairlist_name: str, exchange, pairlistmanager,
config: dict, pairlistconfig: dict, pairlist_pos: int) -> None:
@staticmethod
def load_pairlist(pairlist_name: str, exchange, pairlistmanager,
config: dict, pairlistconfig: dict, pairlist_pos: int) -> IPairList:
"""
Load the custom class from config parameter
:param config: configuration dictionary or None
Load the pairlist with pairlist_name
:param pairlist_name: Classname of the pairlist
:param exchange: Initialized exchange class
:param pairlistmanager: Initialized pairlist manager
:param config: configuration dictionary
:param pairlistconfig: Configuration dedicated to this pairlist
:param pairlist_pos: Position of the pairlist in the list of pairlists
:return: initialized Pairlist class
"""
self.pairlist = self._load_pairlist(pairlist_name, config,
kwargs={'exchange': exchange,
'pairlistmanager': pairlistmanager,
'config': config,
'pairlistconfig': pairlistconfig,
'pairlist_pos': pairlist_pos})
def _load_pairlist(
self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList:
return PairListResolver._load_pairlist(pairlist_name, config,
kwargs={'exchange': exchange,
'pairlistmanager': pairlistmanager,
'config': config,
'pairlistconfig': pairlistconfig,
'pairlist_pos': pairlist_pos})
@staticmethod
def _load_pairlist(pairlist_name: str, config: dict, kwargs: dict) -> IPairList:
"""
Search and loads the specified pairlist.
:param pairlist_name: name of the module to import
@@ -44,11 +50,11 @@ class PairListResolver(IResolver):
"""
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir=None, extra_dir=None)
abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=None, extra_dir=None)
pairlist = self._load_object(paths=abs_paths, object_type=IPairList,
object_name=pairlist_name, kwargs=kwargs)
pairlist = IResolver._load_object(paths=abs_paths, object_type=IPairList,
object_name=pairlist_name, kwargs=kwargs)
if pairlist:
return pairlist
raise OperationalException(

View File

@@ -20,12 +20,11 @@ logger = logging.getLogger(__name__)
class StrategyResolver(IResolver):
"""
This class contains all the logic to load custom strategy class
This class contains the logic to load custom strategy class
"""
__slots__ = ['strategy']
def __init__(self, config: Optional[Dict] = None) -> None:
@staticmethod
def load_strategy(config: Optional[Dict] = None) -> IStrategy:
"""
Load the custom class from config parameter
:param config: configuration dictionary or None
@@ -37,9 +36,9 @@ class StrategyResolver(IResolver):
"the strategy class to use.")
strategy_name = config['strategy']
self.strategy: IStrategy = self._load_strategy(strategy_name,
config=config,
extra_dir=config.get('strategy_path'))
strategy: IStrategy = StrategyResolver._load_strategy(
strategy_name, config=config,
extra_dir=config.get('strategy_path'))
# make sure ask_strategy dict is available
if 'ask_strategy' not in config:
@@ -61,15 +60,18 @@ class StrategyResolver(IResolver):
("stake_currency", None, False),
("stake_amount", None, False),
("startup_candle_count", None, False),
("unfilledtimeout", None, False),
("use_sell_signal", True, True),
("sell_profit_only", False, True),
("ignore_roi_if_buy_signal", False, True),
]
for attribute, default, ask_strategy in attributes:
if ask_strategy:
self._override_attribute_helper(config['ask_strategy'], attribute, default)
StrategyResolver._override_attribute_helper(strategy, config['ask_strategy'],
attribute, default)
else:
self._override_attribute_helper(config, attribute, default)
StrategyResolver._override_attribute_helper(strategy, config,
attribute, default)
# Loop this list again to have output combined
for attribute, _, exp in attributes:
@@ -79,14 +81,16 @@ class StrategyResolver(IResolver):
logger.info("Strategy using %s: %s", attribute, config[attribute])
# Sort and apply type conversions
self.strategy.minimal_roi = OrderedDict(sorted(
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
strategy.minimal_roi = OrderedDict(sorted(
{int(key): value for (key, value) in strategy.minimal_roi.items()}.items(),
key=lambda t: t[0]))
self.strategy.stoploss = float(self.strategy.stoploss)
strategy.stoploss = float(strategy.stoploss)
self._strategy_sanity_validations()
StrategyResolver._strategy_sanity_validations(strategy)
return strategy
def _override_attribute_helper(self, config, attribute: str, default):
@staticmethod
def _override_attribute_helper(strategy, config, attribute: str, default):
"""
Override attributes in the strategy.
Prevalence:
@@ -95,30 +99,32 @@ class StrategyResolver(IResolver):
- default (if not None)
"""
if attribute in config:
setattr(self.strategy, attribute, config[attribute])
setattr(strategy, attribute, config[attribute])
logger.info("Override strategy '%s' with value in config file: %s.",
attribute, config[attribute])
elif hasattr(self.strategy, attribute):
val = getattr(self.strategy, attribute)
elif hasattr(strategy, attribute):
val = getattr(strategy, attribute)
# None's cannot exist in the config, so do not copy them
if val is not None:
config[attribute] = val
# Explicitly check for None here as other "falsy" values are possible
elif default is not None:
setattr(self.strategy, attribute, default)
setattr(strategy, attribute, default)
config[attribute] = default
def _strategy_sanity_validations(self):
if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES):
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
@staticmethod
def _strategy_sanity_validations(strategy):
if not all(k in strategy.order_types for k in constants.REQUIRED_ORDERTYPES):
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
f"Order-types mapping is incomplete.")
if not all(k in self.strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF):
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
if not all(k in strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF):
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
f"Order-time-in-force mapping is incomplete.")
def _load_strategy(
self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy:
@staticmethod
def _load_strategy(strategy_name: str,
config: dict, extra_dir: Optional[str] = None) -> IStrategy:
"""
Search and loads the specified strategy.
:param strategy_name: name of the module to import
@@ -128,9 +134,9 @@ class StrategyResolver(IResolver):
"""
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir=constants.USERPATH_STRATEGY,
extra_dir=extra_dir)
abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=constants.USERPATH_STRATEGY,
extra_dir=extra_dir)
if ":" in strategy_name:
logger.info("loading base64 encoded strategy")
@@ -148,8 +154,8 @@ class StrategyResolver(IResolver):
# register temp path with the bot
abs_paths.insert(0, temp.resolve())
strategy = self._load_object(paths=abs_paths, object_type=IStrategy,
object_name=strategy_name, kwargs={'config': config})
strategy = IResolver._load_object(paths=abs_paths, object_type=IStrategy,
object_name=strategy_name, kwargs={'config': config})
if strategy:
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)

View File

@@ -341,13 +341,14 @@ class RPC:
raise RPCException('All balances are zero.')
symbol = fiat_display_currency
value = self._fiat_converter.convert_amount(total, 'BTC',
value = self._fiat_converter.convert_amount(total, stake_currency,
symbol) if self._fiat_converter else 0
return {
'currencies': output,
'total': total,
'symbol': symbol,
'value': value,
'stake': stake_currency,
'note': 'Simulated balances' if self._freqtrade.config.get('dry_run', False) else ''
}

View File

@@ -335,7 +335,7 @@ class Telegram(RPC):
output = ''
if self._config['dry_run']:
output += (
f"*Warning:*Simulated balances in Dry Mode.\n"
f"*Warning:* Simulated balances in Dry Mode.\n"
"This mode is still experimental!\n"
"Starting capital: "
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
@@ -358,7 +358,7 @@ class Telegram(RPC):
output += curr_output
output += "\n*Estimated Value*:\n" \
"\t`BTC: {total: .8f}`\n" \
"\t`{stake}: {total: .8f}`\n" \
"\t`{symbol}: {value: .2f}`\n".format(**result)
self._send_msg(output)
except RPCException as e:

View File

@@ -168,11 +168,24 @@ class IStrategy(ABC):
"""
Locks pair until a given timestamp happens.
Locked pairs are not analyzed, and are prevented from opening new trades.
Locks can only count up (allowing users to lock pairs for a longer period of time).
To remove a lock from a pair, use `unlock_pair()`
:param pair: Pair to lock
:param until: datetime in UTC until the pair should be blocked from opening new trades.
Needs to be timezone aware `datetime.now(timezone.utc)`
"""
self._pair_locked_until[pair] = until
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
self._pair_locked_until[pair] = until
def unlock_pair(self, pair) -> None:
"""
Unlocks a pair previously locked using lock_pair.
Not used by freqtrade itself, but intended to be used if users lock pairs
manually from within the strategy, to allow an easy way to unlock pairs.
:param pair: Unlock pair to allow trading again
"""
if pair in self._pair_locked_until:
del self._pair_locked_until[pair]
def is_pair_locked(self, pair: str) -> bool:
"""

View File

@@ -73,9 +73,9 @@
"source": [
"# Load strategy using values set above\n",
"from freqtrade.resolvers import StrategyResolver\n",
"strategy = StrategyResolver({'strategy': strategy_name,\n",
" 'user_data_dir': user_data_dir,\n",
" 'strategy_path': strategy_location}).strategy\n",
"strategy = StrategyResolver.load_strategy({'strategy': strategy_name,\n",
" 'user_data_dir': user_data_dir,\n",
" 'strategy_path': strategy_location})\n",
"\n",
"# Generate buy/sell signals using strategy\n",
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",

View File

@@ -191,29 +191,28 @@ def start_download_data(args: Dict[str, Any]) -> None:
"Downloading data requires a list of pairs. "
"Please check the documentation on how to configure this.")
dl_path = Path(config['datadir'])
logger.info(f'About to download pairs: {config["pairs"]}, '
f'intervals: {config["timeframes"]} to {dl_path}')
f'intervals: {config["timeframes"]} to {config["datadir"]}')
pairs_not_available: List[str] = []
# Init exchange
exchange = ExchangeResolver(config['exchange']['name'], config).exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
try:
if config.get('download_trades'):
pairs_not_available = refresh_backtest_trades_data(
exchange, pairs=config["pairs"], datadir=Path(config['datadir']),
exchange, pairs=config["pairs"], datadir=config['datadir'],
timerange=timerange, erase=config.get("erase"))
# Convert downloaded trade data to different timeframes
convert_trades_to_ohlcv(
pairs=config["pairs"], timeframes=config["timeframes"],
datadir=Path(config['datadir']), timerange=timerange, erase=config.get("erase"))
datadir=config['datadir'], timerange=timerange, erase=config.get("erase"))
else:
pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=config["pairs"], timeframes=config["timeframes"],
datadir=Path(config['datadir']), timerange=timerange, erase=config.get("erase"))
datadir=config['datadir'], timerange=timerange, erase=config.get("erase"))
except KeyboardInterrupt:
sys.exit("SIGINT received, aborting ...")
@@ -233,7 +232,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
config['ticker_interval'] = None
# Init exchange
exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
if args['print_one_column']:
print('\n'.join(exchange.timeframes))
@@ -252,7 +251,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
# Init exchange
exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
# By default only active pairs/markets are to be shown
active_only = not args.get('list_pairs_all', False)
@@ -333,7 +332,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
from freqtrade.pairlist.pairlistmanager import PairListManager
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
quote_currencies = args.get('quote_currencies')
if not quote_currencies:

View File

@@ -58,13 +58,15 @@ class Wallets:
- Subtract currently tied up stake_amount in open trades
- update balances for currencies currently in trades
"""
# Recreate _wallets to reset closed trade balances
_wallets = {}
closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all()
open_trades = Trade.get_trades(Trade.is_open.is_(True)).all()
tot_profit = sum([trade.calc_profit() for trade in closed_trades])
tot_in_trades = sum([trade.stake_amount for trade in open_trades])
current_stake = self.start_cap + tot_profit - tot_in_trades
self._wallets[self._config['stake_currency']] = Wallet(
_wallets[self._config['stake_currency']] = Wallet(
self._config['stake_currency'],
current_stake,
0,
@@ -73,12 +75,13 @@ class Wallets:
for trade in open_trades:
curr = trade.pair.split('/')[0]
self._wallets[curr] = Wallet(
_wallets[curr] = Wallet(
curr,
trade.amount,
0,
trade.amount
)
self._wallets = _wallets
def _update_live(self) -> None: