Merge branch 'develop' of https://github.com/theluxaz/freqtrade into main
# Conflicts: # freqtrade/freqtradebot.py # freqtrade/optimize/backtesting.py
This commit is contained in:
@@ -22,7 +22,7 @@ if __version__ == 'develop':
|
||||
# subprocess.check_output(
|
||||
# ['git', 'log', '--format="%h"', '-n 1'],
|
||||
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||
except Exception:
|
||||
except Exception: # pragma: no cover
|
||||
# git not available, ignore
|
||||
try:
|
||||
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
||||
|
@@ -8,14 +8,14 @@ Note: Be careful with file-scoped imports in these subfiles.
|
||||
"""
|
||||
from freqtrade.commands.arguments import Arguments
|
||||
from freqtrade.commands.build_config_commands import start_new_config
|
||||
from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
|
||||
start_list_data)
|
||||
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,
|
||||
start_download_data, start_list_data)
|
||||
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
||||
start_new_hyperopt, start_new_strategy)
|
||||
start_new_strategy)
|
||||
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
||||
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_show_trades)
|
||||
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets,
|
||||
start_list_strategies, start_list_timeframes,
|
||||
start_show_trades)
|
||||
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||
|
@@ -22,7 +22,7 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
|
||||
"max_open_trades", "stake_amount", "fee", "pairs"]
|
||||
|
||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||
"enable_protections", "dry_run_wallet",
|
||||
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
||||
"strategy_list", "export", "exportfilename"]
|
||||
|
||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||
@@ -55,11 +55,11 @@ ARGS_BUILD_CONFIG = ["config"]
|
||||
|
||||
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||
|
||||
ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"]
|
||||
|
||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
|
||||
|
||||
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
||||
|
||||
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
|
||||
|
||||
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange",
|
||||
@@ -73,7 +73,7 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
||||
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
||||
"trade_source", "timeframe", "plot_auto_open"]
|
||||
|
||||
ARGS_INSTALL_UI = ["erase_ui_only"]
|
||||
ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version']
|
||||
|
||||
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
|
||||
|
||||
@@ -92,10 +92,10 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
||||
|
||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||
"list-hyperopts", "hyperopt-list", "hyperopt-show",
|
||||
"plot-dataframe", "plot-profit", "show-trades"]
|
||||
"hyperopt-list", "hyperopt-show",
|
||||
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||
|
||||
|
||||
class Arguments:
|
||||
@@ -171,15 +171,14 @@ class Arguments:
|
||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||
self._build_args(optionlist=['version'], parser=self.parser)
|
||||
|
||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
|
||||
start_download_data, start_edge, start_hyperopt,
|
||||
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
||||
start_list_data, start_list_exchanges, start_list_hyperopts,
|
||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_convert_trades,
|
||||
start_create_userdir, start_download_data, start_edge,
|
||||
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
||||
start_install_ui, start_list_data, start_list_exchanges,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_new_config, start_new_hyperopt,
|
||||
start_new_strategy, start_plot_dataframe, start_plot_profit,
|
||||
start_show_trades, start_test_pairlist, start_trading,
|
||||
start_webserver)
|
||||
start_list_timeframes, start_new_config, start_new_strategy,
|
||||
start_plot_dataframe, start_plot_profit, start_show_trades,
|
||||
start_test_pairlist, start_trading, start_webserver)
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='command',
|
||||
# Use custom message when no subhandler is added
|
||||
@@ -206,12 +205,6 @@ class Arguments:
|
||||
build_config_cmd.set_defaults(func=start_new_config)
|
||||
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
|
||||
|
||||
# add new-hyperopt subcommand
|
||||
build_hyperopt_cmd = subparsers.add_parser('new-hyperopt',
|
||||
help="Create new hyperopt")
|
||||
build_hyperopt_cmd.set_defaults(func=start_new_hyperopt)
|
||||
self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd)
|
||||
|
||||
# add new-strategy subcommand
|
||||
build_strategy_cmd = subparsers.add_parser('new-strategy',
|
||||
help="Create new strategy")
|
||||
@@ -245,6 +238,15 @@ class Arguments:
|
||||
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
||||
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
|
||||
|
||||
# Add trades-to-ohlcv subcommand
|
||||
convert_trade_data_cmd = subparsers.add_parser(
|
||||
'trades-to-ohlcv',
|
||||
help='Convert trade data to OHLCV data.',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
convert_trade_data_cmd.set_defaults(func=start_convert_trades)
|
||||
self._build_args(optionlist=ARGS_CONVERT_TRADES, parser=convert_trade_data_cmd)
|
||||
|
||||
# Add list-data subcommand
|
||||
list_data_cmd = subparsers.add_parser(
|
||||
'list-data',
|
||||
@@ -300,15 +302,6 @@ class Arguments:
|
||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
|
||||
|
||||
# Add list-hyperopts subcommand
|
||||
list_hyperopts_cmd = subparsers.add_parser(
|
||||
'list-hyperopts',
|
||||
help='Print available hyperopt classes.',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
list_hyperopts_cmd.set_defaults(func=start_list_hyperopts)
|
||||
self._build_args(optionlist=ARGS_LIST_HYPEROPTS, parser=list_hyperopts_cmd)
|
||||
|
||||
# Add list-markets subcommand
|
||||
list_markets_cmd = subparsers.add_parser(
|
||||
'list-markets',
|
||||
|
@@ -61,21 +61,27 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"type": "text",
|
||||
"name": "stake_currency",
|
||||
"message": "Please insert your stake currency:",
|
||||
"default": 'BTC',
|
||||
"default": 'USDT',
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "stake_amount",
|
||||
"message": "Please insert your stake amount:",
|
||||
"default": "0.01",
|
||||
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||
"default": "100",
|
||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
||||
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
||||
if val == UNLIMITED_STAKE_AMOUNT
|
||||
else val
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "max_open_trades",
|
||||
"message": f"Please insert max_open_trades (Integer or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||
"default": "3",
|
||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_int(val)
|
||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_int(val),
|
||||
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
||||
if val == UNLIMITED_STAKE_AMOUNT
|
||||
else val
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
@@ -99,6 +105,8 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"bittrex",
|
||||
"kraken",
|
||||
"ftx",
|
||||
"kucoin",
|
||||
"gateio",
|
||||
Separator(),
|
||||
"other",
|
||||
],
|
||||
@@ -122,6 +130,12 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"message": "Insert Exchange Secret",
|
||||
"when": lambda x: not x['dry_run']
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_key_password",
|
||||
"message": "Insert Exchange API Key password",
|
||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] == 'kucoin'
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "telegram",
|
||||
@@ -149,7 +163,8 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
{
|
||||
"type": "text",
|
||||
"name": "api_server_listen_addr",
|
||||
"message": "Insert Api server Listen Address (best left untouched default!)",
|
||||
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, "
|
||||
"otherwise best left untouched)"),
|
||||
"default": "127.0.0.1",
|
||||
"when": lambda x: x['api_server']
|
||||
},
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Definition of cli arguments used in arguments.py
|
||||
"""
|
||||
from argparse import ArgumentTypeError
|
||||
from argparse import SUPPRESS, ArgumentTypeError
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
|
||||
@@ -135,6 +135,10 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help='Override the value of the `stake_amount` configuration setting.',
|
||||
),
|
||||
# Backtesting
|
||||
"timeframe_detail": Arg(
|
||||
'--timeframe-detail',
|
||||
help='Specify detail timeframe for backtesting (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||
),
|
||||
"position_stacking": Arg(
|
||||
'--eps', '--enable-position-stacking',
|
||||
help='Allow buying the same pair multiple times (position stacking).',
|
||||
@@ -199,13 +203,13 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
# Hyperopt
|
||||
"hyperopt": Arg(
|
||||
'--hyperopt',
|
||||
help='Specify hyperopt class name which will be used by the bot.',
|
||||
help=SUPPRESS,
|
||||
metavar='NAME',
|
||||
required=False,
|
||||
),
|
||||
"hyperopt_path": Arg(
|
||||
'--hyperopt-path',
|
||||
help='Specify additional lookup path for Hyperopt and Hyperopt Loss functions.',
|
||||
help='Specify additional lookup path for Hyperopt Loss functions.',
|
||||
metavar='PATH',
|
||||
),
|
||||
"epochs": Arg(
|
||||
@@ -377,12 +381,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"dataformat_ohlcv": Arg(
|
||||
'--data-format-ohlcv',
|
||||
help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).',
|
||||
help='Storage format for downloaded candle (OHLCV) data. (default: `json`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
),
|
||||
"dataformat_trades": Arg(
|
||||
'--data-format-trades',
|
||||
help='Storage format for downloaded trades data. (default: `%(default)s`).',
|
||||
help='Storage format for downloaded trades data. (default: `jsongz`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
),
|
||||
"exchange": Arg(
|
||||
@@ -410,6 +414,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
action='store_true',
|
||||
default=False,
|
||||
),
|
||||
"ui_version": Arg(
|
||||
'--ui-version',
|
||||
help=('Specify a specific version of FreqUI to install. '
|
||||
'Not specifying this installs the latest version.'),
|
||||
type=str,
|
||||
),
|
||||
# Templating options
|
||||
"template": Arg(
|
||||
'--template',
|
||||
|
@@ -89,6 +89,41 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
f"on exchange {exchange.name}.")
|
||||
|
||||
|
||||
def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
|
||||
timerange = TimeRange()
|
||||
|
||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||
config['stake_currency'] = ''
|
||||
|
||||
if 'pairs' not in config:
|
||||
raise OperationalException(
|
||||
"Downloading data requires a list of pairs. "
|
||||
"Please check the documentation on how to configure this.")
|
||||
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||
# Manual validations of relevant settings
|
||||
if not config['exchange'].get('skip_pair_validation', False):
|
||||
exchange.validate_pairs(config['pairs'])
|
||||
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
|
||||
|
||||
logger.info(f"About to Convert pairs: {expanded_pairs}, "
|
||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||
|
||||
for timeframe in config['timeframes']:
|
||||
exchange.validate_timeframes(timeframe)
|
||||
# Convert downloaded trade data to different timeframes
|
||||
convert_trades_to_ohlcv(
|
||||
pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
||||
data_format_ohlcv=config['dataformat_ohlcv'],
|
||||
data_format_trades=config['dataformat_trades'],
|
||||
)
|
||||
|
||||
|
||||
def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
||||
"""
|
||||
Convert data from one format to another
|
||||
|
@@ -7,7 +7,7 @@ import requests
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
|
||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
||||
from freqtrade.constants import USERPATH_STRATEGIES
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import render_template, render_template_with_fallback
|
||||
@@ -87,56 +87,6 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
||||
raise OperationalException("`new-strategy` requires --strategy to be set.")
|
||||
|
||||
|
||||
def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: str) -> None:
|
||||
"""
|
||||
Deploys a new hyperopt template to hyperopt_path
|
||||
"""
|
||||
fallback = 'full'
|
||||
buy_guards = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2",
|
||||
)
|
||||
sell_guards = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2",
|
||||
)
|
||||
buy_space = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2",
|
||||
)
|
||||
sell_space = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2",
|
||||
)
|
||||
|
||||
strategy_text = render_template(templatefile='base_hyperopt.py.j2',
|
||||
arguments={"hyperopt": hyperopt_name,
|
||||
"buy_guards": buy_guards,
|
||||
"sell_guards": sell_guards,
|
||||
"buy_space": buy_space,
|
||||
"sell_space": sell_space,
|
||||
})
|
||||
|
||||
logger.info(f"Writing hyperopt to `{hyperopt_path}`.")
|
||||
hyperopt_path.write_text(strategy_text)
|
||||
|
||||
|
||||
def start_new_hyperopt(args: Dict[str, Any]) -> None:
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
if 'hyperopt' in args and args['hyperopt']:
|
||||
|
||||
new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args['hyperopt'] + '.py')
|
||||
|
||||
if new_path.exists():
|
||||
raise OperationalException(f"`{new_path}` already exists. "
|
||||
"Please choose another Hyperopt Name.")
|
||||
deploy_new_hyperopt(args['hyperopt'], new_path, args['template'])
|
||||
else:
|
||||
raise OperationalException("`new-hyperopt` requires --hyperopt to be set.")
|
||||
|
||||
|
||||
def clean_ui_subdir(directory: Path):
|
||||
if directory.is_dir():
|
||||
logger.info("Removing UI directory content.")
|
||||
@@ -178,7 +128,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
|
||||
f.write(version)
|
||||
|
||||
|
||||
def get_ui_download_url() -> Tuple[str, str]:
|
||||
def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]:
|
||||
base_url = 'https://api.github.com/repos/freqtrade/frequi/'
|
||||
# Get base UI Repo path
|
||||
|
||||
@@ -186,8 +136,16 @@ def get_ui_download_url() -> Tuple[str, str]:
|
||||
resp.raise_for_status()
|
||||
r = resp.json()
|
||||
|
||||
latest_version = r[0]['name']
|
||||
assets = r[0].get('assets', [])
|
||||
if version:
|
||||
tmp = [x for x in r if x['name'] == version]
|
||||
if tmp:
|
||||
latest_version = tmp[0]['name']
|
||||
assets = tmp[0].get('assets', [])
|
||||
else:
|
||||
raise ValueError("UI-Version not found.")
|
||||
else:
|
||||
latest_version = r[0]['name']
|
||||
assets = r[0].get('assets', [])
|
||||
dl_url = ''
|
||||
if assets and len(assets) > 0:
|
||||
dl_url = assets[0]['browser_download_url']
|
||||
@@ -206,7 +164,7 @@ def start_install_ui(args: Dict[str, Any]) -> None:
|
||||
|
||||
dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/'
|
||||
# First make sure the assets are removed.
|
||||
dl_url, latest_version = get_ui_download_url()
|
||||
dl_url, latest_version = get_ui_download_url(args.get('ui_version'))
|
||||
|
||||
curr_version = read_ui_version(dest_folder)
|
||||
if curr_version == latest_version and not args.get('erase_ui_only'):
|
||||
|
@@ -53,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
|
||||
if epochs and export_csv:
|
||||
HyperoptTools.export_csv_file(
|
||||
config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv
|
||||
config, epochs, export_csv
|
||||
)
|
||||
|
||||
|
||||
|
@@ -10,7 +10,7 @@ from colorama import init as colorama_init
|
||||
from tabulate import tabulate
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
||||
from freqtrade.constants import USERPATH_STRATEGIES
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import market_is_active, validate_exchanges
|
||||
@@ -92,25 +92,6 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||
_print_objs_tabular(strategy_objs, config.get('print_colorized', False))
|
||||
|
||||
|
||||
def start_list_hyperopts(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Print files with HyperOpt custom classes available in the directory
|
||||
"""
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
directory = Path(config.get('hyperopt_path', config['user_data_dir'] / USERPATH_HYPEROPTS))
|
||||
hyperopt_objs = HyperOptResolver.search_all_objects(directory, not args['print_one_column'])
|
||||
# Sort alphabetically
|
||||
hyperopt_objs = sorted(hyperopt_objs, key=lambda x: x['name'])
|
||||
|
||||
if args['print_one_column']:
|
||||
print('\n'.join([s['name'] for s in hyperopt_objs]))
|
||||
else:
|
||||
_print_objs_tabular(hyperopt_objs, config.get('print_colorized', False))
|
||||
|
||||
|
||||
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Print timeframes available on Exchange
|
||||
|
19
freqtrade/configuration/PeriodicCache.py
Normal file
19
freqtrade/configuration/PeriodicCache.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from cachetools.ttl import TTLCache
|
||||
|
||||
|
||||
class PeriodicCache(TTLCache):
|
||||
"""
|
||||
Special cache that expires at "straight" times
|
||||
A timer with ttl of 3600 (1h) will expire at every full hour (:00).
|
||||
"""
|
||||
|
||||
def __init__(self, maxsize, ttl, getsizeof=None):
|
||||
def local_timer():
|
||||
ts = datetime.now(timezone.utc).timestamp()
|
||||
offset = (ts % ttl)
|
||||
return ts - offset
|
||||
|
||||
# Init with smlight offset
|
||||
super().__init__(maxsize=maxsize, ttl=ttl-1e-5, timer=local_timer, getsizeof=getsizeof)
|
@@ -1,7 +1,8 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.configuration.check_exchange import check_exchange, remove_credentials
|
||||
from freqtrade.configuration.check_exchange import check_exchange
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||
from freqtrade.configuration.configuration import Configuration
|
||||
from freqtrade.configuration.PeriodicCache import PeriodicCache
|
||||
from freqtrade.configuration.timerange import TimeRange
|
||||
|
@@ -10,19 +10,6 @@ from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt,
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def remove_credentials(config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Removes exchange keys from the configuration and specifies dry-run
|
||||
Used for backtesting / hyperopt / edge and utils.
|
||||
Modifies the input dict!
|
||||
"""
|
||||
config['exchange']['key'] = ''
|
||||
config['exchange']['secret'] = ''
|
||||
config['exchange']['password'] = ''
|
||||
config['exchange']['uid'] = ''
|
||||
config['dry_run'] = True
|
||||
|
||||
|
||||
def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
||||
"""
|
||||
Check if the exchange name in the config file is supported by Freqtrade
|
||||
|
@@ -3,7 +3,6 @@ from typing import Any, Dict
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
|
||||
from .check_exchange import remove_credentials
|
||||
from .config_validation import validate_config_consistency
|
||||
from .configuration import Configuration
|
||||
|
||||
@@ -21,8 +20,8 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
|
||||
configuration = Configuration(args, method)
|
||||
config = configuration.get_config()
|
||||
|
||||
# Ensure we do not use Exchange credentials
|
||||
remove_credentials(config)
|
||||
# Ensure these modes are using Dry-run
|
||||
config['dry_run'] = True
|
||||
validate_config_consistency(config)
|
||||
|
||||
return config
|
||||
|
@@ -242,6 +242,9 @@ class Configuration:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self._args_to_config(config, argname='timeframe_detail',
|
||||
logstring='Parameter --timeframe-detail detected, '
|
||||
'using {} for intra-candle backtesting ...')
|
||||
self._args_to_config(config, argname='stake_amount',
|
||||
logstring='Parameter --stake-amount detected, '
|
||||
'overriding stake_amount to: {} ...')
|
||||
|
@@ -24,7 +24,8 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
|
||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
||||
'MaxDrawDownHyperOptLoss']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||
@@ -69,9 +70,7 @@ DUST_PER_COIN = {
|
||||
# Source files with destination directories within user-directory
|
||||
USER_DATA_FILES = {
|
||||
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||
'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS,
|
||||
'sample_hyperopt_loss.py': USERPATH_HYPEROPTS,
|
||||
'sample_hyperopt.py': USERPATH_HYPEROPTS,
|
||||
'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS,
|
||||
}
|
||||
|
||||
@@ -112,7 +111,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'tradable_balance_ratio': {
|
||||
'type': 'number',
|
||||
'minimum': 0.1,
|
||||
'minimum': 0.0,
|
||||
'maximum': 1,
|
||||
'default': 0.99
|
||||
},
|
||||
@@ -286,6 +285,15 @@ CONF_SCHEMA = {
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
},
|
||||
'protection_trigger': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
},
|
||||
'protection_trigger_global': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
},
|
||||
}
|
||||
},
|
||||
'reload': {'type': 'boolean'},
|
||||
|
@@ -149,6 +149,8 @@ class DataProvider:
|
||||
Clear pair dataframe cache.
|
||||
"""
|
||||
self.__cached_pairs = {}
|
||||
self.__cached_pairs_backtesting = {}
|
||||
self.__slice_index = 0
|
||||
|
||||
# Exchange functions
|
||||
|
||||
|
@@ -197,7 +197,8 @@ def _download_pair_history(pair: str, *,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms if since_ms else
|
||||
arrow.utcnow().shift(
|
||||
days=-new_pairs_days).int_timestamp * 1000
|
||||
days=-new_pairs_days).int_timestamp * 1000,
|
||||
is_new_pair=data.empty
|
||||
)
|
||||
# TODO: Maybe move parsing to exchange class (?)
|
||||
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
|
||||
|
@@ -119,7 +119,7 @@ class Edge:
|
||||
)
|
||||
# Download informative pairs too
|
||||
res = defaultdict(list)
|
||||
for p, t in self.strategy.informative_pairs():
|
||||
for p, t in self.strategy.gather_informative_pairs():
|
||||
res[t].append(p)
|
||||
for timeframe, inf_pairs in res.items():
|
||||
timerange_startup = deepcopy(self._timerange)
|
||||
|
@@ -11,6 +11,8 @@ class RPCMessageType(Enum):
|
||||
SELL = 'sell'
|
||||
SELL_FILL = 'sell_fill'
|
||||
SELL_CANCEL = 'sell_cancel'
|
||||
PROTECTION_TRIGGER = 'protection_trigger'
|
||||
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||
|
||||
def __repr__(self):
|
||||
return self.value
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# flake8: noqa: F401
|
||||
# isort: off
|
||||
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.exchange import Exchange
|
||||
# isort: on
|
||||
from freqtrade.exchange.bibox import Bibox
|
||||
|
@@ -1,7 +1,8 @@
|
||||
""" Binance exchange subclass """
|
||||
import logging
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
@@ -18,6 +19,7 @@ class Binance(Exchange):
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "fromId",
|
||||
@@ -89,3 +91,20 @@ class Binance(Exchange):
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, is_new_pair: bool
|
||||
) -> List:
|
||||
"""
|
||||
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
||||
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
||||
"""
|
||||
if is_new_pair:
|
||||
x = await self._async_get_candle_history(pair, timeframe, 0)
|
||||
if x and x[2] and x[2][0] and x[2][0][0] > since_ms:
|
||||
# Set starting date to first available candle.
|
||||
since_ms = x[2][0][0]
|
||||
logger.info(f"Candle-data for {pair} available starting with "
|
||||
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
||||
return await super()._async_get_historic_ohlcv(
|
||||
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)
|
||||
|
@@ -51,6 +51,19 @@ EXCHANGE_HAS_OPTIONAL = [
|
||||
]
|
||||
|
||||
|
||||
def remove_credentials(config) -> None:
|
||||
"""
|
||||
Removes exchange keys from the configuration and specifies dry-run
|
||||
Used for backtesting / hyperopt / edge and utils.
|
||||
Modifies the input dict!
|
||||
"""
|
||||
if config.get('dry_run', False):
|
||||
config['exchange']['key'] = ''
|
||||
config['exchange']['secret'] = ''
|
||||
config['exchange']['password'] = ''
|
||||
config['exchange']['uid'] = ''
|
||||
|
||||
|
||||
def calculate_backoff(retrycount, max_retries):
|
||||
"""
|
||||
Calculate backoff
|
||||
|
@@ -26,9 +26,9 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
|
||||
InvalidOrderException, OperationalException, PricingError,
|
||||
RetryableOrderError, TemporaryError)
|
||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
||||
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier,
|
||||
retrier_async)
|
||||
from freqtrade.misc import deep_merge_dicts, safe_value_fallback2
|
||||
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
||||
remove_credentials, retrier, retrier_async)
|
||||
from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
|
||||
|
||||
@@ -54,12 +54,16 @@ class Exchange:
|
||||
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
||||
_params: Dict = {}
|
||||
|
||||
# Additional headers - added to the ccxt object
|
||||
_headers: Dict = {}
|
||||
|
||||
# Dict to specify which options each exchange implements
|
||||
# This defines defaults, which can be selectively overridden by subclasses using _ft_has
|
||||
# or by specifying them in the configuration.
|
||||
_ft_has_default: Dict = {
|
||||
"stoploss_on_exchange": False,
|
||||
"order_time_in_force": ["gtc"],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_candle_limit": 500,
|
||||
"ohlcv_partial_candle": True,
|
||||
@@ -100,6 +104,7 @@ class Exchange:
|
||||
|
||||
# Holds all open sell orders for dry_run
|
||||
self._dry_run_open_orders: Dict[str, Any] = {}
|
||||
remove_credentials(config)
|
||||
|
||||
if config['dry_run']:
|
||||
logger.info('Instance is running with dry_run enabled')
|
||||
@@ -169,7 +174,7 @@ class Exchange:
|
||||
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||
|
||||
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
||||
ccxt_kwargs: dict = None) -> ccxt.Exchange:
|
||||
ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
|
||||
"""
|
||||
Initialize ccxt with given config and return valid
|
||||
ccxt instance.
|
||||
@@ -188,6 +193,10 @@ class Exchange:
|
||||
}
|
||||
if ccxt_kwargs:
|
||||
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
|
||||
if self._headers:
|
||||
# Inject static headers after the above output to not confuse users.
|
||||
ccxt_kwargs = deep_merge_dicts({'headers': self._headers}, ccxt_kwargs)
|
||||
if ccxt_kwargs:
|
||||
ex_config.update(ccxt_kwargs)
|
||||
try:
|
||||
|
||||
@@ -352,9 +361,16 @@ class Exchange:
|
||||
def validate_stakecurrency(self, stake_currency: str) -> None:
|
||||
"""
|
||||
Checks stake-currency against available currencies on the exchange.
|
||||
Only runs on startup. If markets have not been loaded, there's been a problem with
|
||||
the connection to the exchange.
|
||||
:param stake_currency: Stake-currency to validate
|
||||
:raise: OperationalException if stake-currency is not available.
|
||||
"""
|
||||
if not self._markets:
|
||||
raise OperationalException(
|
||||
'Could not load markets, therefore cannot start. '
|
||||
'Please investigate the above error for more details.'
|
||||
)
|
||||
quote_currencies = self.get_quote_currencies()
|
||||
if stake_currency not in quote_currencies:
|
||||
raise OperationalException(
|
||||
@@ -464,7 +480,7 @@ class Exchange:
|
||||
if startup_candles + 5 > candle_limit:
|
||||
raise OperationalException(
|
||||
f"This strategy requires {startup_candles} candles to start. "
|
||||
f"{self.name} only provides {candle_limit} for {timeframe}.")
|
||||
f"{self.name} only provides {candle_limit - 5} for {timeframe}.")
|
||||
|
||||
def exchange_has(self, endpoint: str) -> bool:
|
||||
"""
|
||||
@@ -507,7 +523,7 @@ class Exchange:
|
||||
precision = self.markets[pair]['precision']['price']
|
||||
missing = price % precision
|
||||
if missing != 0:
|
||||
price = price - missing + precision
|
||||
price = round(price - missing + precision, 10)
|
||||
else:
|
||||
symbol_prec = self.markets[pair]['precision']['price']
|
||||
big_price = price * pow(10, symbol_prec)
|
||||
@@ -709,7 +725,8 @@ class Exchange:
|
||||
|
||||
params = self._params.copy()
|
||||
if time_in_force != 'gtc' and ordertype != 'market':
|
||||
params.update({'timeInForce': time_in_force})
|
||||
param = self._ft_has.get('time_in_force_parameter', '')
|
||||
params.update({param: time_in_force})
|
||||
|
||||
try:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
@@ -1041,7 +1058,7 @@ class Exchange:
|
||||
ticker_rate = ticker[conf_strategy['price_side']]
|
||||
if ticker['last'] and ticker_rate:
|
||||
if side == 'buy' and ticker_rate > ticker['last']:
|
||||
balance = conf_strategy['ask_last_balance']
|
||||
balance = conf_strategy.get('ask_last_balance', 0.0)
|
||||
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
||||
elif side == 'sell' and ticker_rate < ticker['last']:
|
||||
balance = conf_strategy.get('bid_last_balance', 0.0)
|
||||
@@ -1178,7 +1195,7 @@ class Exchange:
|
||||
# Historic data
|
||||
|
||||
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int) -> List:
|
||||
since_ms: int, is_new_pair: bool = False) -> List:
|
||||
"""
|
||||
Get candle history using asyncio and returns the list of candles.
|
||||
Handles all async work for this.
|
||||
@@ -1190,7 +1207,7 @@ class Exchange:
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
||||
since_ms=since_ms))
|
||||
since_ms=since_ms, is_new_pair=is_new_pair))
|
||||
|
||||
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
||||
since_ms: int) -> DataFrame:
|
||||
@@ -1205,11 +1222,12 @@ class Exchange:
|
||||
return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str,
|
||||
timeframe: str,
|
||||
since_ms: int) -> List:
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, is_new_pair: bool
|
||||
) -> List:
|
||||
"""
|
||||
Download historic ohlcv
|
||||
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
||||
"""
|
||||
|
||||
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
||||
@@ -1222,21 +1240,22 @@ class Exchange:
|
||||
pair, timeframe, since) for since in
|
||||
range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)]
|
||||
|
||||
results = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||
|
||||
# Combine gathered results
|
||||
data: List = []
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
continue
|
||||
# Deconstruct tuple if it's not an exception
|
||||
p, _, new_data = res
|
||||
if p == pair:
|
||||
data.extend(new_data)
|
||||
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
||||
for input_coro in chunks(input_coroutines, 100):
|
||||
|
||||
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
continue
|
||||
# Deconstruct tuple if it's not an exception
|
||||
p, _, new_data = res
|
||||
if p == pair:
|
||||
data.extend(new_data)
|
||||
# Sort data again after extending the result - above calls return in "async order"
|
||||
data = sorted(data, key=lambda x: x[0])
|
||||
logger.info("Downloaded data for %s with length %s.", pair, len(data))
|
||||
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||
return data
|
||||
|
||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
@@ -21,3 +22,12 @@ class Gateio(Exchange):
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 1000,
|
||||
}
|
||||
|
||||
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
|
||||
|
||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
||||
super().validate_ordertypes(order_types)
|
||||
|
||||
if any(v == 'market' for k, v in order_types.items()):
|
||||
raise OperationalException(
|
||||
f'Exchange {self.name} does not support market orders.')
|
||||
|
@@ -21,4 +21,6 @@ class Kucoin(Exchange):
|
||||
_ft_has: Dict = {
|
||||
"l2_limit_range": [20, 100],
|
||||
"l2_limit_range_required": False,
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
}
|
||||
|
@@ -83,10 +83,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
||||
|
||||
# Attach Dataprovider to Strategy baseclass
|
||||
IStrategy.dp = self.dataprovider
|
||||
# Attach Wallets to Strategy baseclass
|
||||
IStrategy.wallets = self.wallets
|
||||
# Attach Dataprovider to strategy instance
|
||||
self.strategy.dp = self.dataprovider
|
||||
# Attach Wallets to strategy instance
|
||||
self.strategy.wallets = self.wallets
|
||||
|
||||
# Initializing Edge only if enabled
|
||||
self.edge = Edge(self.config, self.exchange, self.strategy) if \
|
||||
@@ -99,7 +99,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
||||
|
||||
# Protect sell-logic from forcesell and vice versa
|
||||
self._sell_lock = Lock()
|
||||
self._exit_lock = Lock()
|
||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||
|
||||
def notify_status(self, msg: str) -> None:
|
||||
@@ -139,7 +139,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# Only update open orders on startup
|
||||
# This will update the database after the initial migration
|
||||
self.update_open_orders()
|
||||
self.startup_update_open_orders()
|
||||
|
||||
def process(self) -> None:
|
||||
"""
|
||||
@@ -160,20 +160,20 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# Refreshing candles
|
||||
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
||||
self.strategy.informative_pairs())
|
||||
self.strategy.gather_informative_pairs())
|
||||
|
||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
||||
|
||||
self.strategy.analyze(self.active_pair_whitelist)
|
||||
|
||||
with self._sell_lock:
|
||||
with self._exit_lock:
|
||||
# Check and handle any timed out open orders
|
||||
self.check_handle_timedout()
|
||||
|
||||
# Protect from collisions with forcesell.
|
||||
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
||||
# while selling is in process, since telegram messages arrive in an different thread.
|
||||
with self._sell_lock:
|
||||
with self._exit_lock:
|
||||
trades = Trade.get_open_trades()
|
||||
# First process current opened trades (positions)
|
||||
self.exit_positions(trades)
|
||||
@@ -237,7 +237,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
open_trades = len(Trade.get_open_trades())
|
||||
return max(0, self.config['max_open_trades'] - open_trades)
|
||||
|
||||
def update_open_orders(self):
|
||||
def startup_update_open_orders(self):
|
||||
"""
|
||||
Updates open orders based on order list kept in the database.
|
||||
Mainly updates the state of orders - but may also close trades
|
||||
@@ -296,9 +296,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
if sell_order:
|
||||
self.refind_lost_order(trade)
|
||||
else:
|
||||
self.reupdate_buy_order_fees(trade)
|
||||
self.reupdate_enter_order_fees(trade)
|
||||
|
||||
def reupdate_buy_order_fees(self, trade: Trade):
|
||||
def reupdate_enter_order_fees(self, trade: Trade):
|
||||
"""
|
||||
Get buy order from database, and try to reupdate.
|
||||
Handles trades where the initial fee-update did not work.
|
||||
@@ -476,21 +476,21 @@ class FreqtradeBot(LoggingMixin):
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
|
||||
if price:
|
||||
buy_limit_requested = price
|
||||
enter_limit_requested = price
|
||||
else:
|
||||
# Calculate price
|
||||
proposed_buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||
default_retval=proposed_buy_rate)(
|
||||
default_retval=proposed_enter_rate)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
proposed_rate=proposed_buy_rate)
|
||||
proposed_rate=proposed_enter_rate)
|
||||
|
||||
buy_limit_requested = self.get_valid_price(custom_entry_price, proposed_buy_rate)
|
||||
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
|
||||
|
||||
if not buy_limit_requested:
|
||||
if not enter_limit_requested:
|
||||
raise PricingError('Could not determine buy price.')
|
||||
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested,
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested,
|
||||
self.strategy.stoploss)
|
||||
|
||||
if not self.edge:
|
||||
@@ -498,7 +498,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||
default_retval=stake_amount)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
current_rate=buy_limit_requested, proposed_stake=stake_amount,
|
||||
current_rate=enter_limit_requested, proposed_stake=stake_amount,
|
||||
min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
|
||||
@@ -508,27 +508,27 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
||||
f"{stake_amount} ...")
|
||||
|
||||
amount = stake_amount / buy_limit_requested
|
||||
amount = stake_amount / enter_limit_requested
|
||||
order_type = self.strategy.order_types['buy']
|
||||
if forcebuy:
|
||||
# Forcebuy can define a different ordertype
|
||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||
pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested,
|
||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)):
|
||||
logger.info(f"User requested abortion of buying {pair}")
|
||||
return False
|
||||
amount = self.exchange.amount_to_precision(pair, amount)
|
||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||
amount=amount, rate=buy_limit_requested,
|
||||
amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force)
|
||||
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
|
||||
order_id = order['id']
|
||||
order_status = order.get('status', None)
|
||||
|
||||
# we assume the order is executed at the price requested
|
||||
buy_limit_filled_price = buy_limit_requested
|
||||
enter_limit_filled_price = enter_limit_requested
|
||||
amount_requested = amount
|
||||
|
||||
if order_status == 'expired' or order_status == 'rejected':
|
||||
@@ -551,13 +551,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
stake_amount = order['cost']
|
||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||
|
||||
# in case of FOK the order may be filled immediately and fully
|
||||
elif order_status == 'closed':
|
||||
stake_amount = order['cost']
|
||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||
|
||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||
@@ -569,8 +569,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
amount_requested=amount_requested,
|
||||
fee_open=fee,
|
||||
fee_close=fee,
|
||||
open_rate=buy_limit_filled_price,
|
||||
open_rate_requested=buy_limit_requested,
|
||||
open_rate=enter_limit_filled_price,
|
||||
open_rate_requested=enter_limit_requested,
|
||||
open_date=datetime.utcnow(),
|
||||
exchange=self.exchange.id,
|
||||
open_order_id=order_id,
|
||||
@@ -590,11 +590,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Updating wallets
|
||||
self.wallets.update()
|
||||
|
||||
self._notify_buy(trade, order_type)
|
||||
self._notify_enter(trade, order_type)
|
||||
|
||||
return True
|
||||
|
||||
def _notify_buy(self, trade: Trade, order_type: str) -> None:
|
||||
def _notify_enter(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a buy occurred.
|
||||
"""
|
||||
@@ -617,7 +617,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||
def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a buy cancel occurred.
|
||||
"""
|
||||
@@ -643,7 +643,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_buy_fill(self, trade: Trade) -> None:
|
||||
def _notify_enter_fill(self, trade: Trade) -> None:
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_FILL,
|
||||
@@ -746,7 +746,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
except InvalidOrderException as e:
|
||||
trade.stoploss_order_id = None
|
||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||
logger.warning('Selling the trade forcefully')
|
||||
logger.warning('Exiting the trade forcefully')
|
||||
self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
||||
sell_type=SellType.EMERGENCY_SELL))
|
||||
|
||||
@@ -784,7 +784,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
self._notify_sell(trade, "stoploss")
|
||||
self._notify_exit(trade, "stoploss")
|
||||
return True
|
||||
|
||||
if trade.open_order_id or not trade.is_open:
|
||||
@@ -853,20 +853,20 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.warning(f"Could not create trailing stoploss order "
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
||||
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
||||
buy: bool, sell: bool, sell_tag: Optional[str]) -> bool:
|
||||
"""
|
||||
Check and execute sell
|
||||
Check and execute exit
|
||||
"""
|
||||
|
||||
should_sell = self.strategy.should_sell(
|
||||
trade, sell_rate, datetime.now(timezone.utc), buy, sell,
|
||||
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
|
||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||
)
|
||||
|
||||
if should_sell.sell_flag:
|
||||
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. Tag: {sell_tag if sell_tag is not None else "None"}')
|
||||
self.execute_trade_exit(trade, sell_rate, should_sell,sell_tag)
|
||||
self.execute_trade_exit(trade, exit_rate, should_sell,sell_tag)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -909,7 +909,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
default_retval=False)(pair=trade.pair,
|
||||
trade=trade,
|
||||
order=order))):
|
||||
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
|
||||
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
||||
fully_cancelled
|
||||
@@ -918,7 +918,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
default_retval=False)(pair=trade.pair,
|
||||
trade=trade,
|
||||
order=order))):
|
||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
|
||||
def cancel_all_open_orders(self) -> None:
|
||||
"""
|
||||
@@ -934,13 +934,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
continue
|
||||
|
||||
if order['side'] == 'buy':
|
||||
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
|
||||
elif order['side'] == 'sell':
|
||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
Trade.commit()
|
||||
|
||||
def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool:
|
||||
def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool:
|
||||
"""
|
||||
Buy cancel - cancel order
|
||||
:return: True if order was fully cancelled
|
||||
@@ -997,11 +997,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||
|
||||
self.wallets.update()
|
||||
self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'],
|
||||
reason=reason)
|
||||
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'],
|
||||
reason=reason)
|
||||
return was_trade_fully_canceled
|
||||
|
||||
def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str:
|
||||
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str:
|
||||
"""
|
||||
Sell cancel - cancel order and update trade
|
||||
:return: Reason for cancel
|
||||
@@ -1035,14 +1035,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
|
||||
self.wallets.update()
|
||||
self._notify_sell_cancel(
|
||||
self._notify_exit_cancel(
|
||||
trade,
|
||||
order_type=self.strategy.order_types['sell'],
|
||||
reason=reason
|
||||
)
|
||||
return reason
|
||||
|
||||
def _safe_sell_amount(self, pair: str, amount: float) -> float:
|
||||
def _safe_exit_amount(self, pair: str, amount: float) -> float:
|
||||
"""
|
||||
Get sellable amount.
|
||||
Should be trade.amount - but will fall back to the available amount if necessary.
|
||||
@@ -1114,7 +1114,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# but we allow this value to be changed)
|
||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
||||
|
||||
amount = self._safe_sell_amount(trade.pair, trade.amount)
|
||||
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||
time_in_force = self.strategy.order_time_in_force['sell']
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
@@ -1155,11 +1155,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
|
||||
self._notify_sell(trade, order_type)
|
||||
self._notify_exit(trade, order_type)
|
||||
|
||||
return True
|
||||
|
||||
def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None:
|
||||
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell occurred.
|
||||
"""
|
||||
@@ -1201,7 +1201,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell cancel occurred.
|
||||
"""
|
||||
@@ -1222,7 +1222,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
'limit': profit_rate,
|
||||
'limit': profit_rate or 0,
|
||||
'order_type': order_type,
|
||||
'amount': trade.amount,
|
||||
'open_rate': trade.open_rate,
|
||||
@@ -1231,7 +1231,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'profit_ratio': profit_ratio,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date,
|
||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'reason': reason,
|
||||
@@ -1296,16 +1296,28 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Updating wallets when order is closed
|
||||
if not trade.is_open:
|
||||
if not stoploss_order and not trade.open_order_id:
|
||||
self._notify_sell(trade, '', True)
|
||||
self.protections.stop_per_pair(trade.pair)
|
||||
self.protections.global_stop()
|
||||
self._notify_exit(trade, '', True)
|
||||
self.handle_protections(trade.pair)
|
||||
self.wallets.update()
|
||||
elif not trade.open_order_id:
|
||||
# Buy fill
|
||||
self._notify_buy_fill(trade)
|
||||
self._notify_enter_fill(trade)
|
||||
|
||||
return False
|
||||
|
||||
def handle_protections(self, pair: str) -> None:
|
||||
prot_trig = self.protections.stop_per_pair(pair)
|
||||
if prot_trig:
|
||||
msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
|
||||
msg.update(prot_trig.to_json())
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
prot_trig_glb = self.protections.global_stop()
|
||||
if prot_trig_glb:
|
||||
msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
|
||||
msg.update(prot_trig_glb.to_json())
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
|
||||
amount: float, fee_abs: float) -> float:
|
||||
"""
|
||||
|
@@ -87,7 +87,7 @@ def setup_logging(config: Dict[str, Any]) -> None:
|
||||
# syslog config. The messages should be equal for this.
|
||||
handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
|
||||
logging.root.addHandler(handler_sl)
|
||||
elif s[0] == 'journald':
|
||||
elif s[0] == 'journald': # pragma: no cover
|
||||
try:
|
||||
from systemd.journal import JournaldLogHandler
|
||||
except ImportError:
|
||||
|
@@ -9,7 +9,7 @@ from typing import Any, List
|
||||
|
||||
|
||||
# check min. python version
|
||||
if sys.version_info < (3, 7):
|
||||
if sys.version_info < (3, 7): # pragma: no cover
|
||||
sys.exit("Freqtrade requires Python version >= 3.7")
|
||||
|
||||
from freqtrade.commands import Arguments
|
||||
@@ -46,7 +46,7 @@ def main(sysargv: List[str] = None) -> None:
|
||||
"`freqtrade --help` or `freqtrade <command> --help`."
|
||||
)
|
||||
|
||||
except SystemExit as e:
|
||||
except SystemExit as e: # pragma: no cover
|
||||
return_code = e
|
||||
except KeyboardInterrupt:
|
||||
logger.info('SIGINT received, aborting ...')
|
||||
@@ -60,5 +60,5 @@ def main(sysargv: List[str] = None) -> None:
|
||||
sys.exit(return_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
main()
|
||||
|
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
|
||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
||||
@@ -61,8 +61,7 @@ class Backtesting:
|
||||
self.config = config
|
||||
self.results: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Reset keys for backtesting
|
||||
remove_credentials(self.config)
|
||||
config['dry_run'] = True
|
||||
self.strategylist: List[IStrategy] = []
|
||||
self.all_results: Dict[str, Dict] = {}
|
||||
|
||||
@@ -86,7 +85,7 @@ class Backtesting:
|
||||
"configuration or as cli argument `--timeframe 5m`")
|
||||
self.timeframe = str(self.config.get('timeframe'))
|
||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||
|
||||
self.init_backtest_detail()
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
if 'VolumePairList' in self.pairlists.name_list:
|
||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||
@@ -109,14 +108,6 @@ class Backtesting:
|
||||
else:
|
||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||
|
||||
Trade.use_db = False
|
||||
Trade.reset_trades()
|
||||
PairLocks.timeframe = self.config['timeframe']
|
||||
PairLocks.use_db = False
|
||||
PairLocks.reset_locks()
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange, log=False)
|
||||
|
||||
self.timerange = TimeRange.parse_timerange(
|
||||
None if self.config.get('timerange') is None else str(self.config.get('timerange')))
|
||||
|
||||
@@ -125,9 +116,7 @@ class Backtesting:
|
||||
# Add maximum startup candle count to configuration for informative pairs support
|
||||
self.config['startup_candle_count'] = self.required_startup
|
||||
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
|
||||
|
||||
self.progress = BTProgress()
|
||||
self.abort = False
|
||||
self.init_backtest()
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
@@ -137,6 +126,28 @@ class Backtesting:
|
||||
PairLocks.use_db = True
|
||||
Trade.use_db = True
|
||||
|
||||
def init_backtest_detail(self):
|
||||
# Load detail timeframe if specified
|
||||
self.timeframe_detail = str(self.config.get('timeframe_detail', ''))
|
||||
if self.timeframe_detail:
|
||||
self.timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail)
|
||||
if self.timeframe_min <= self.timeframe_detail_min:
|
||||
raise OperationalException(
|
||||
"Detail timeframe must be smaller than strategy timeframe.")
|
||||
|
||||
else:
|
||||
self.timeframe_detail_min = 0
|
||||
self.detail_data: Dict[str, DataFrame] = {}
|
||||
|
||||
def init_backtest(self):
|
||||
|
||||
self.prepare_backtest(False)
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange, log=False)
|
||||
|
||||
self.progress = BTProgress()
|
||||
self.abort = False
|
||||
|
||||
def _set_strategy(self, strategy: IStrategy):
|
||||
"""
|
||||
Load strategy into backtesting
|
||||
@@ -144,7 +155,7 @@ class Backtesting:
|
||||
self.strategy: IStrategy = strategy
|
||||
strategy.dp = self.dataprovider
|
||||
# Attach Wallets to Strategy baseclass
|
||||
IStrategy.wallets = self.wallets
|
||||
strategy.wallets = self.wallets
|
||||
# Set stoploss_on_exchange to false for backtesting,
|
||||
# since a "perfect" stoploss-sell is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
@@ -188,6 +199,23 @@ class Backtesting:
|
||||
self.progress.set_new_value(1)
|
||||
return data, self.timerange
|
||||
|
||||
def load_bt_data_detail(self) -> None:
|
||||
"""
|
||||
Loads backtest detail data (smaller timeframe) if necessary.
|
||||
"""
|
||||
if self.timeframe_detail:
|
||||
self.detail_data = history.load_data(
|
||||
datadir=self.config['datadir'],
|
||||
pairs=self.pairlists.whitelist,
|
||||
timeframe=self.timeframe_detail,
|
||||
timerange=self.timerange,
|
||||
startup_candles=0,
|
||||
fail_without_data=True,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
)
|
||||
else:
|
||||
self.detail_data = {}
|
||||
|
||||
def prepare_backtest(self, enable_protections):
|
||||
"""
|
||||
Backtesting setup method - called once for every call to "backtest()".
|
||||
@@ -199,7 +227,8 @@ class Backtesting:
|
||||
Trade.reset_trades()
|
||||
self.rejected_trades = 0
|
||||
self.dataprovider.clear_cache()
|
||||
self._load_protections(self.strategy)
|
||||
if enable_protections:
|
||||
self._load_protections(self.strategy)
|
||||
|
||||
def check_abort(self):
|
||||
"""
|
||||
@@ -320,10 +349,8 @@ class Backtesting:
|
||||
else:
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
|
||||
|
||||
|
||||
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
|
||||
sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
|
||||
sell_candle_time, sell_row[BUY_IDX],
|
||||
@@ -353,6 +380,32 @@ class Backtesting:
|
||||
|
||||
return None
|
||||
|
||||
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
if self.timeframe_detail and trade.pair in self.detail_data:
|
||||
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||
sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min)
|
||||
|
||||
detail_data = self.detail_data[trade.pair]
|
||||
detail_data = detail_data.loc[
|
||||
(detail_data['date'] >= sell_candle_time) &
|
||||
(detail_data['date'] < sell_candle_end)
|
||||
].copy()
|
||||
if len(detail_data) == 0:
|
||||
# Fall back to "regular" data if no detail data was found for this candle
|
||||
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||
detail_data.loc[:, 'buy'] = sell_row[BUY_IDX]
|
||||
detail_data.loc[:, 'sell'] = sell_row[SELL_IDX]
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||
for det_row in detail_data[headers].values.tolist():
|
||||
res = self._get_sell_trade_entry_for_candle(trade, det_row)
|
||||
if res:
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
else:
|
||||
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||
|
||||
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
|
||||
try:
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
||||
@@ -601,6 +654,7 @@ class Backtesting:
|
||||
data: Dict[str, Any] = {}
|
||||
|
||||
data, timerange = self.load_bt_data()
|
||||
self.load_bt_data_detail()
|
||||
logger.info("Dataload complete. Calculating indicators")
|
||||
|
||||
for strat in self.strategylist:
|
||||
|
@@ -7,7 +7,8 @@ import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
|
||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.optimize.optimize_reports import generate_edge_table
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
@@ -28,11 +29,12 @@ class EdgeCli:
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
|
||||
# Reset keys for edge
|
||||
remove_credentials(self.config)
|
||||
# Ensure using dry-run
|
||||
self.config['dry_run'] = True
|
||||
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||
self.strategy.dp = DataProvider(config, None)
|
||||
|
||||
validate_config_consistency(self.config)
|
||||
|
||||
|
@@ -22,6 +22,7 @@ from pandas import DataFrame
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
|
||||
from freqtrade.data.converter import trim_dataframes
|
||||
from freqtrade.data.history import get_timerange
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||
@@ -30,7 +31,7 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
|
||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
|
||||
|
||||
# Suppress scikit-learn FutureWarnings from skopt
|
||||
@@ -44,7 +45,7 @@ progressbar.streams.wrap_stdout()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
INITIAL_POINTS = 30
|
||||
INITIAL_POINTS = 5
|
||||
|
||||
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
||||
# in the skopt model queue, to optimize memory consumption
|
||||
@@ -78,10 +79,10 @@ class Hyperopt:
|
||||
|
||||
if not self.config.get('hyperopt'):
|
||||
self.custom_hyperopt = HyperOptAuto(self.config)
|
||||
self.auto_hyperopt = True
|
||||
else:
|
||||
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
|
||||
self.auto_hyperopt = False
|
||||
raise OperationalException(
|
||||
"Using separate Hyperopt files has been removed in 2021.9. Please convert "
|
||||
"your existing Hyperopt file to the new Hyperoptable strategy interface")
|
||||
|
||||
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
||||
self.custom_hyperopt.strategy = self.backtesting.strategy
|
||||
@@ -103,31 +104,6 @@ class Hyperopt:
|
||||
self.num_epochs_saved = 0
|
||||
self.current_best_epoch: Optional[Dict[str, Any]] = None
|
||||
|
||||
if not self.auto_hyperopt:
|
||||
# Populate "fallback" functions here
|
||||
# (hasattr is slow so should not be run during "regular" operations)
|
||||
if hasattr(self.custom_hyperopt, 'populate_indicators'):
|
||||
logger.warning(
|
||||
"DEPRECATED: Using `populate_indicators()` in the hyperopt file is deprecated. "
|
||||
"Please move these methods to your strategy."
|
||||
)
|
||||
self.backtesting.strategy.populate_indicators = ( # type: ignore
|
||||
self.custom_hyperopt.populate_indicators) # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
||||
logger.warning(
|
||||
"DEPRECATED: Using `populate_buy_trend()` in the hyperopt file is deprecated. "
|
||||
"Please move these methods to your strategy."
|
||||
)
|
||||
self.backtesting.strategy.populate_buy_trend = ( # type: ignore
|
||||
self.custom_hyperopt.populate_buy_trend) # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
||||
logger.warning(
|
||||
"DEPRECATED: Using `populate_sell_trend()` in the hyperopt file is deprecated. "
|
||||
"Please move these methods to your strategy."
|
||||
)
|
||||
self.backtesting.strategy.populate_sell_trend = ( # type: ignore
|
||||
self.custom_hyperopt.populate_sell_trend) # type: ignore
|
||||
|
||||
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
||||
if self.config.get('use_max_market_positions', True):
|
||||
self.max_open_trades = self.config['max_open_trades']
|
||||
@@ -256,7 +232,7 @@ class Hyperopt:
|
||||
"""
|
||||
Assign the dimensions in the hyperoptimization space.
|
||||
"""
|
||||
if self.auto_hyperopt and HyperoptTools.has_space(self.config, 'protection'):
|
||||
if HyperoptTools.has_space(self.config, 'protection'):
|
||||
# Protections can only be optimized when using the Parameter interface
|
||||
logger.debug("Hyperopt has 'protection' space")
|
||||
# Enable Protections if protection space is selected.
|
||||
@@ -265,7 +241,7 @@ class Hyperopt:
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'buy'):
|
||||
logger.debug("Hyperopt has 'buy' space")
|
||||
self.buy_space = self.custom_hyperopt.indicator_space()
|
||||
self.buy_space = self.custom_hyperopt.buy_indicator_space()
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
logger.debug("Hyperopt has 'sell' space")
|
||||
@@ -285,6 +261,15 @@ class Hyperopt:
|
||||
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
||||
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
||||
|
||||
def assign_params(self, params_dict: Dict, category: str) -> None:
|
||||
"""
|
||||
Assign hyperoptable parameters
|
||||
"""
|
||||
for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category):
|
||||
if attr.optimize:
|
||||
# noinspection PyProtectedMember
|
||||
attr.value = params_dict[attr_name]
|
||||
|
||||
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
|
||||
"""
|
||||
Used Optimize function.
|
||||
@@ -296,18 +281,13 @@ class Hyperopt:
|
||||
|
||||
# Apply parameters
|
||||
if HyperoptTools.has_space(self.config, 'buy'):
|
||||
self.backtesting.strategy.advise_buy = ( # type: ignore
|
||||
self.custom_hyperopt.buy_strategy_generator(params_dict))
|
||||
self.assign_params(params_dict, 'buy')
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
self.backtesting.strategy.advise_sell = ( # type: ignore
|
||||
self.custom_hyperopt.sell_strategy_generator(params_dict))
|
||||
self.assign_params(params_dict, 'sell')
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'protection'):
|
||||
for attr_name, attr in self.backtesting.strategy.enumerate_parameters('protection'):
|
||||
if attr.optimize:
|
||||
# noinspection PyProtectedMember
|
||||
attr.value = params_dict[attr_name]
|
||||
self.assign_params(params_dict, 'protection')
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'roi'):
|
||||
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
||||
@@ -385,10 +365,20 @@ class Hyperopt:
|
||||
}
|
||||
|
||||
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:
|
||||
estimator = self.custom_hyperopt.generate_estimator()
|
||||
|
||||
acq_optimizer = "sampling"
|
||||
if isinstance(estimator, str):
|
||||
if estimator not in ("GP", "RF", "ET", "GBRT"):
|
||||
raise OperationalException(f"Estimator {estimator} not supported.")
|
||||
else:
|
||||
acq_optimizer = "auto"
|
||||
|
||||
logger.info(f"Using estimator {estimator}.")
|
||||
return Optimizer(
|
||||
dimensions,
|
||||
base_estimator="ET",
|
||||
acq_optimizer="auto",
|
||||
base_estimator=estimator,
|
||||
acq_optimizer=acq_optimizer,
|
||||
n_initial_points=INITIAL_POINTS,
|
||||
acq_optimizer_kwargs={'n_jobs': cpu_count},
|
||||
random_state=self.random_state,
|
||||
@@ -517,11 +507,10 @@ class Hyperopt:
|
||||
f"saved to '{self.results_file}'.")
|
||||
|
||||
if self.current_best_epoch:
|
||||
if self.auto_hyperopt:
|
||||
HyperoptTools.try_export_params(
|
||||
self.config,
|
||||
self.backtesting.strategy.get_strategy_name(),
|
||||
self.current_best_epoch)
|
||||
HyperoptTools.try_export_params(
|
||||
self.config,
|
||||
self.backtesting.strategy.get_strategy_name(),
|
||||
self.current_best_epoch)
|
||||
|
||||
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
|
||||
self.print_json)
|
||||
|
@@ -4,15 +4,23 @@ This module implements a convenience auto-hyperopt class, which can be used toge
|
||||
that implement IHyperStrategy interface.
|
||||
"""
|
||||
from contextlib import suppress
|
||||
from typing import Any, Callable, Dict, List
|
||||
from typing import Callable, Dict, List
|
||||
|
||||
from pandas import DataFrame
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
from skopt.space import Dimension
|
||||
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||
from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt
|
||||
|
||||
|
||||
def _format_exception_message(space: str) -> str:
|
||||
raise OperationalException(
|
||||
f"The '{space}' space is included into the hyperoptimization "
|
||||
f"but no parameter for this space was not found in your Strategy. "
|
||||
f"Please make sure to have parameters for this space enabled for optimization "
|
||||
f"or remove the '{space}' space from hyperoptimization.")
|
||||
|
||||
|
||||
class HyperOptAuto(IHyperOpt):
|
||||
@@ -22,26 +30,6 @@ class HyperOptAuto(IHyperOpt):
|
||||
sell_indicator_space methods, but other hyperopt methods can be overridden as well.
|
||||
"""
|
||||
|
||||
def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict):
|
||||
for attr_name, attr in self.strategy.enumerate_parameters('buy'):
|
||||
if attr.optimize:
|
||||
# noinspection PyProtectedMember
|
||||
attr.value = params[attr_name]
|
||||
return self.strategy.populate_buy_trend(dataframe, metadata)
|
||||
|
||||
return populate_buy_trend
|
||||
|
||||
def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict):
|
||||
for attr_name, attr in self.strategy.enumerate_parameters('sell'):
|
||||
if attr.optimize:
|
||||
# noinspection PyProtectedMember
|
||||
attr.value = params[attr_name]
|
||||
return self.strategy.populate_sell_trend(dataframe, metadata)
|
||||
|
||||
return populate_sell_trend
|
||||
|
||||
def _get_func(self, name) -> Callable:
|
||||
"""
|
||||
Return a function defined in Strategy.HyperOpt class, or one defined in super() class.
|
||||
@@ -60,21 +48,22 @@ class HyperOptAuto(IHyperOpt):
|
||||
if attr.optimize:
|
||||
yield attr.get_space(attr_name)
|
||||
|
||||
def _get_indicator_space(self, category, fallback_method_name):
|
||||
def _get_indicator_space(self, category):
|
||||
# TODO: is this necessary, or can we call "generate_space" directly?
|
||||
indicator_space = list(self._generate_indicator_space(category))
|
||||
if len(indicator_space) > 0:
|
||||
return indicator_space
|
||||
else:
|
||||
return self._get_func(fallback_method_name)()
|
||||
_format_exception_message(category)
|
||||
|
||||
def indicator_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('buy', 'indicator_space')
|
||||
def buy_indicator_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('buy')
|
||||
|
||||
def sell_indicator_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('sell', 'sell_indicator_space')
|
||||
return self._get_indicator_space('sell')
|
||||
|
||||
def protection_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('protection', 'protection_space')
|
||||
return self._get_indicator_space('protection')
|
||||
|
||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||
return self._get_func('generate_roi_table')(params)
|
||||
@@ -90,3 +79,6 @@ class HyperOptAuto(IHyperOpt):
|
||||
|
||||
def trailing_space(self) -> List['Dimension']:
|
||||
return self._get_func('trailing_space')()
|
||||
|
||||
def generate_estimator(self) -> EstimatorType:
|
||||
return self._get_func('generate_estimator')()
|
||||
|
@@ -5,11 +5,11 @@ This module defines the interface to apply for hyperopt
|
||||
import logging
|
||||
import math
|
||||
from abc import ABC
|
||||
from typing import Any, Callable, Dict, List
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from sklearn.base import RegressorMixin
|
||||
from skopt.space import Categorical, Dimension, Integer
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.misc import round_dict
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
@@ -18,12 +18,7 @@ from freqtrade.strategy import IStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _format_exception_message(method: str, space: str) -> str:
|
||||
return (f"The '{space}' space is included into the hyperoptimization "
|
||||
f"but {method}() method is not found in your "
|
||||
f"custom Hyperopt class. You should either implement this "
|
||||
f"method or remove the '{space}' space from hyperoptimization.")
|
||||
EstimatorType = Union[RegressorMixin, str]
|
||||
|
||||
|
||||
class IHyperOpt(ABC):
|
||||
@@ -45,36 +40,13 @@ class IHyperOpt(ABC):
|
||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
||||
IHyperOpt.timeframe = str(config['timeframe'])
|
||||
|
||||
def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
||||
def generate_estimator(self) -> EstimatorType:
|
||||
"""
|
||||
Create a buy strategy generator.
|
||||
Return base_estimator.
|
||||
Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class
|
||||
inheriting from RegressorMixin (from sklearn).
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy'))
|
||||
|
||||
def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Create a sell strategy generator.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell'))
|
||||
|
||||
def protection_space(self) -> List[Dimension]:
|
||||
"""
|
||||
Create a protection space.
|
||||
Only supported by the Parameter interface.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('indicator_space', 'protection'))
|
||||
|
||||
def indicator_space(self) -> List[Dimension]:
|
||||
"""
|
||||
Create an indicator space.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('indicator_space', 'buy'))
|
||||
|
||||
def sell_indicator_space(self) -> List[Dimension]:
|
||||
"""
|
||||
Create a sell indicator space.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('sell_indicator_space', 'sell'))
|
||||
return 'ET'
|
||||
|
||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||
"""
|
||||
|
41
freqtrade/optimize/hyperopt_loss_max_drawdown.py
Normal file
41
freqtrade/optimize/hyperopt_loss_max_drawdown.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
MaxDrawDownHyperOptLoss
|
||||
|
||||
This module defines the alternative HyperOptLoss class which can be used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.btanalysis import calculate_max_drawdown
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
class MaxDrawDownHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
"""
|
||||
Defines the loss function for hyperopt.
|
||||
|
||||
This implementation optimizes for max draw down and profit
|
||||
Less max drawdown more profit -> Lower return value
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
*args, **kwargs) -> float:
|
||||
|
||||
"""
|
||||
Objective function.
|
||||
|
||||
Uses profit ratio weighted max_drawdown when drawdown is available.
|
||||
Otherwise directly optimizes profit ratio.
|
||||
"""
|
||||
total_profit = results['profit_abs'].sum()
|
||||
try:
|
||||
max_drawdown = calculate_max_drawdown(results, value_col='profit_abs')
|
||||
except ValueError:
|
||||
# No losing trade, therefore no drawdown.
|
||||
return -total_profit
|
||||
return -total_profit / max_drawdown[0]
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import rapidjson
|
||||
import tabulate
|
||||
from colorama import Fore, Style
|
||||
@@ -298,8 +299,8 @@ class HyperoptTools():
|
||||
f"Objective: {results['loss']:.5f}")
|
||||
|
||||
@staticmethod
|
||||
def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str:
|
||||
|
||||
def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool,
|
||||
has_drawdown: bool) -> pd.DataFrame:
|
||||
trials['Best'] = ''
|
||||
|
||||
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
||||
@@ -435,8 +436,7 @@ class HyperoptTools():
|
||||
return table
|
||||
|
||||
@staticmethod
|
||||
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
|
||||
csv_file: str) -> None:
|
||||
def export_csv_file(config: dict, results: list, csv_file: str) -> None:
|
||||
"""
|
||||
Log result to csv-file
|
||||
"""
|
||||
|
@@ -464,6 +464,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
'max_open_trades_setting': (config['max_open_trades']
|
||||
if config['max_open_trades'] != float('inf') else -1),
|
||||
'timeframe': config['timeframe'],
|
||||
'timeframe_detail': config.get('timeframe_detail', ''),
|
||||
'timerange': config.get('timerange', ''),
|
||||
'enable_protections': config.get('enable_protections', False),
|
||||
'strategy_name': strategy,
|
||||
|
@@ -2,7 +2,7 @@
|
||||
This module contains the class to persist trades into SQLite
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -835,17 +835,21 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
return total_open_stake_amount or 0
|
||||
|
||||
@staticmethod
|
||||
def get_overall_performance() -> List[Dict[str, Any]]:
|
||||
def get_overall_performance(minutes=None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, including profit and trade count
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if minutes:
|
||||
start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
|
||||
filters.append(Trade.close_date >= start_date)
|
||||
pair_rates = Trade.query.with_entities(
|
||||
Trade.pair,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(Trade.is_open.is_(False))\
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.pair) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
@@ -30,7 +30,8 @@ class PairLocks():
|
||||
PairLocks.locks = []
|
||||
|
||||
@staticmethod
|
||||
def lock_pair(pair: str, until: datetime, reason: str = None, *, now: datetime = None) -> None:
|
||||
def lock_pair(pair: str, until: datetime, reason: str = None, *,
|
||||
now: datetime = None) -> PairLock:
|
||||
"""
|
||||
Create PairLock from now to "until".
|
||||
Uses database by default, unless PairLocks.use_db is set to False,
|
||||
@@ -52,6 +53,7 @@ class PairLocks():
|
||||
PairLock.query.session.commit()
|
||||
else:
|
||||
PairLocks.locks.append(lock)
|
||||
return lock
|
||||
|
||||
@staticmethod
|
||||
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]:
|
||||
|
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import PeriodicCache
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
@@ -18,14 +19,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class AgeFilter(IPairList):
|
||||
|
||||
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
||||
_symbolsChecked: Dict[str, int] = {}
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
||||
self._symbolsChecked: Dict[str, int] = {}
|
||||
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
||||
|
||||
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
|
||||
|
||||
@@ -69,9 +71,12 @@ class AgeFilter(IPairList):
|
||||
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
||||
:return: new allowlist
|
||||
"""
|
||||
needed_pairs = [(p, '1d') for p in pairlist if p not in self._symbolsChecked]
|
||||
needed_pairs = [
|
||||
(p, '1d') for p in pairlist
|
||||
if p not in self._symbolsChecked and p not in self._symbolsCheckFailed]
|
||||
if not needed_pairs:
|
||||
return pairlist
|
||||
# Remove pairs that have been removed before
|
||||
return [p for p in pairlist if p not in self._symbolsCheckFailed]
|
||||
|
||||
since_days = -(
|
||||
self._max_days_listed if self._max_days_listed else self._min_days_listed
|
||||
@@ -118,5 +123,6 @@ class AgeFilter(IPairList):
|
||||
" or more than "
|
||||
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
||||
) if self._max_days_listed else ''), logger.info)
|
||||
self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000
|
||||
return False
|
||||
return False
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Performance pair list filter
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pandas as pd
|
||||
|
||||
@@ -15,6 +15,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class PerformanceFilter(IPairList):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
self._minutes = pairlistconfig.get('minutes', 0)
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
@@ -40,7 +47,7 @@ class PerformanceFilter(IPairList):
|
||||
"""
|
||||
# Get the trading performance for pairs from database
|
||||
try:
|
||||
performance = pd.DataFrame(Trade.get_overall_performance())
|
||||
performance = pd.DataFrame(Trade.get_overall_performance(self._minutes))
|
||||
except AttributeError:
|
||||
# Performancefilter does not work in backtesting.
|
||||
self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
|
||||
|
@@ -123,7 +123,7 @@ class VolumePairList(IPairList):
|
||||
filtered_tickers = [
|
||||
v for k, v in tickers.items()
|
||||
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||
and v[self._sort_key] is not None)]
|
||||
and (self._use_range or v[self._sort_key] is not None))]
|
||||
pairlist = [s['symbol'] for s in filtered_tickers]
|
||||
|
||||
pairlist = self.filter_pairlist(pairlist, tickers)
|
||||
|
@@ -17,7 +17,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
|
||||
if keep_invalid:
|
||||
for pair_wc in wildcardpl:
|
||||
try:
|
||||
comp = re.compile(pair_wc)
|
||||
comp = re.compile(pair_wc, re.IGNORECASE)
|
||||
result_partial = [
|
||||
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
||||
]
|
||||
@@ -33,7 +33,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
|
||||
else:
|
||||
for pair_wc in wildcardpl:
|
||||
try:
|
||||
comp = re.compile(pair_wc)
|
||||
comp = re.compile(pair_wc, re.IGNORECASE)
|
||||
result += [
|
||||
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
||||
]
|
||||
|
@@ -6,6 +6,7 @@ from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from freqtrade.persistence import PairLocks
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from freqtrade.plugins.protections import IProtection
|
||||
from freqtrade.resolvers import ProtectionResolver
|
||||
|
||||
@@ -43,30 +44,28 @@ class ProtectionManager():
|
||||
"""
|
||||
return [{p.name: p.short_desc()} for p in self._protection_handlers]
|
||||
|
||||
def global_stop(self, now: Optional[datetime] = None) -> bool:
|
||||
def global_stop(self, now: Optional[datetime] = None) -> Optional[PairLock]:
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
result = False
|
||||
result = None
|
||||
for protection_handler in self._protection_handlers:
|
||||
if protection_handler.has_global_stop:
|
||||
result, until, reason = protection_handler.global_stop(now)
|
||||
lock, until, reason = protection_handler.global_stop(now)
|
||||
|
||||
# Early stopping - first positive result blocks further trades
|
||||
if result and until:
|
||||
if lock and until:
|
||||
if not PairLocks.is_global_lock(until):
|
||||
PairLocks.lock_pair('*', until, reason, now=now)
|
||||
result = True
|
||||
result = PairLocks.lock_pair('*', until, reason, now=now)
|
||||
return result
|
||||
|
||||
def stop_per_pair(self, pair, now: Optional[datetime] = None) -> bool:
|
||||
def stop_per_pair(self, pair, now: Optional[datetime] = None) -> Optional[PairLock]:
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
result = False
|
||||
result = None
|
||||
for protection_handler in self._protection_handlers:
|
||||
if protection_handler.has_local_stop:
|
||||
result, until, reason = protection_handler.stop_per_pair(pair, now)
|
||||
if result and until:
|
||||
lock, until, reason = protection_handler.stop_per_pair(pair, now)
|
||||
if lock and until:
|
||||
if not PairLocks.is_pair_locked(pair, until):
|
||||
PairLocks.lock_pair(pair, until, reason, now=now)
|
||||
result = True
|
||||
result = PairLocks.lock_pair(pair, until, reason, now=now)
|
||||
return result
|
||||
|
@@ -9,7 +9,6 @@ from typing import Dict
|
||||
|
||||
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
||||
from freqtrade.resolvers import IResolver
|
||||
|
||||
@@ -17,43 +16,6 @@ from freqtrade.resolvers import IResolver
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HyperOptResolver(IResolver):
|
||||
"""
|
||||
This class contains all the logic to load custom hyperopt class
|
||||
"""
|
||||
object_type = IHyperOpt
|
||||
object_type_str = "Hyperopt"
|
||||
user_subdir = USERPATH_HYPEROPTS
|
||||
initial_search_path = None
|
||||
|
||||
@staticmethod
|
||||
def load_hyperopt(config: Dict) -> IHyperOpt:
|
||||
"""
|
||||
Load the custom hyperopt class from config parameter
|
||||
:param config: configuration dictionary
|
||||
"""
|
||||
if not config.get('hyperopt'):
|
||||
raise OperationalException("No Hyperopt set. Please use `--hyperopt` to specify "
|
||||
"the Hyperopt class to use.")
|
||||
|
||||
hyperopt_name = config['hyperopt']
|
||||
|
||||
hyperopt = HyperOptResolver.load_object(hyperopt_name, config,
|
||||
kwargs={'config': config},
|
||||
extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
if not hasattr(hyperopt, 'populate_indicators'):
|
||||
logger.info("Hyperopt class does not provide populate_indicators() method. "
|
||||
"Using populate_indicators from the strategy.")
|
||||
if not hasattr(hyperopt, 'populate_buy_trend'):
|
||||
logger.info("Hyperopt class does not provide populate_buy_trend() method. "
|
||||
"Using populate_buy_trend from the strategy.")
|
||||
if not hasattr(hyperopt, 'populate_sell_trend'):
|
||||
logger.info("Hyperopt class does not provide populate_sell_trend() method. "
|
||||
"Using populate_sell_trend from the strategy.")
|
||||
return hyperopt
|
||||
|
||||
|
||||
class HyperOptLossResolver(IResolver):
|
||||
"""
|
||||
This class contains all the logic to load custom hyperopt loss class
|
||||
|
@@ -4,6 +4,7 @@ from copy import deepcopy
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||
from freqtrade.enums import BacktestState
|
||||
from freqtrade.exceptions import DependencyException
|
||||
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
|
||||
@@ -42,35 +43,40 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
||||
# Reload strategy
|
||||
lastconfig = ApiServer._bt_last_config
|
||||
strat = StrategyResolver.load_strategy(btconfig)
|
||||
validate_config_consistency(btconfig)
|
||||
|
||||
if (
|
||||
not ApiServer._bt
|
||||
or lastconfig.get('timeframe') != strat.timeframe
|
||||
or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)
|
||||
or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail')
|
||||
or lastconfig.get('timerange') != btconfig['timerange']
|
||||
):
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
ApiServer._bt = Backtesting(btconfig)
|
||||
|
||||
if ApiServer._bt.timeframe_detail:
|
||||
ApiServer._bt.load_bt_data_detail()
|
||||
else:
|
||||
ApiServer._bt.config = btconfig
|
||||
ApiServer._bt.init_backtest()
|
||||
# Only reload data if timeframe changed.
|
||||
if (
|
||||
not ApiServer._bt_data
|
||||
or not ApiServer._bt_timerange
|
||||
or lastconfig.get('stake_amount') != btconfig.get('stake_amount')
|
||||
or lastconfig.get('enable_protections') != btconfig.get('enable_protections')
|
||||
or lastconfig.get('protections') != btconfig.get('protections', [])
|
||||
or lastconfig.get('timeframe') != strat.timeframe
|
||||
or lastconfig.get('timerange') != btconfig['timerange']
|
||||
):
|
||||
lastconfig['timerange'] = btconfig['timerange']
|
||||
lastconfig['protections'] = btconfig.get('protections', [])
|
||||
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
||||
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
||||
lastconfig['timeframe'] = strat.timeframe
|
||||
ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data()
|
||||
|
||||
lastconfig['timerange'] = btconfig['timerange']
|
||||
lastconfig['timeframe'] = strat.timeframe
|
||||
lastconfig['protections'] = btconfig.get('protections', [])
|
||||
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
||||
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
||||
|
||||
ApiServer._bt.abort = False
|
||||
min_date, max_date = ApiServer._bt.backtest_one_strategy(
|
||||
strat, ApiServer._bt_data, ApiServer._bt_timerange)
|
||||
|
||||
ApiServer._bt.results = generate_backtest_stats(
|
||||
ApiServer._bt_data, ApiServer._bt.all_results,
|
||||
min_date=min_date, max_date=max_date)
|
||||
|
@@ -46,6 +46,12 @@ class Balances(BaseModel):
|
||||
value: float
|
||||
stake: str
|
||||
note: str
|
||||
starting_capital: float
|
||||
starting_capital_ratio: float
|
||||
starting_capital_pct: float
|
||||
starting_capital_fiat: float
|
||||
starting_capital_fiat_ratio: float
|
||||
starting_capital_fiat_pct: float
|
||||
|
||||
|
||||
class Count(BaseModel):
|
||||
@@ -324,6 +330,7 @@ class PairHistory(BaseModel):
|
||||
class BacktestRequest(BaseModel):
|
||||
strategy: str
|
||||
timeframe: Optional[str]
|
||||
timeframe_detail: Optional[str]
|
||||
timerange: Optional[str]
|
||||
max_open_trades: Optional[int]
|
||||
stake_amount: Optional[Union[float, str]]
|
||||
@@ -340,3 +347,8 @@ class BacktestResponse(BaseModel):
|
||||
trade_count: Optional[float]
|
||||
# TODO: Properly type backtestresult...
|
||||
backtest_result: Optional[Dict[str, Any]]
|
||||
|
||||
|
||||
class SysInfo(BaseModel):
|
||||
cpu_pct: List[float]
|
||||
ram_pct: float
|
||||
|
@@ -18,7 +18,8 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac
|
||||
OpenTradeSchema, PairHistory, PerformanceEntry,
|
||||
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
|
||||
Stats, StatusMsg, StrategyListResponse,
|
||||
StrategyResponse, Version, WhitelistResponse)
|
||||
StrategyResponse, SysInfo, Version,
|
||||
WhitelistResponse)
|
||||
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
|
||||
@@ -259,3 +260,8 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option
|
||||
'pair_interval': pair_interval,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@router.get('/sysinfo', response_model=SysInfo, tags=['info'])
|
||||
def sysinfo():
|
||||
return RPC._rpc_sysinfo()
|
||||
|
@@ -5,6 +5,20 @@ import time
|
||||
import uvicorn
|
||||
|
||||
|
||||
def asyncio_setup() -> None: # pragma: no cover
|
||||
# Set eventloop for win32 setups
|
||||
# Reverts a change done in uvicorn 0.15.0 - which now sets the eventloop
|
||||
# via policy.
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 8) and sys.platform == "win32":
|
||||
import asyncio
|
||||
import selectors
|
||||
selector = selectors.SelectSelector()
|
||||
loop = asyncio.SelectorEventLoop(selector)
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
|
||||
class UvicornServer(uvicorn.Server):
|
||||
"""
|
||||
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
|
||||
@@ -28,7 +42,7 @@ class UvicornServer(uvicorn.Server):
|
||||
try:
|
||||
import uvloop # noqa
|
||||
except ImportError: # pragma: no cover
|
||||
from uvicorn.loops.asyncio import asyncio_setup
|
||||
|
||||
asyncio_setup()
|
||||
else:
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
|
@@ -8,6 +8,7 @@ from math import isnan
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import arrow
|
||||
import psutil
|
||||
from numpy import NAN, inf, int64, mean
|
||||
from pandas import DataFrame
|
||||
|
||||
@@ -403,8 +404,11 @@ class RPC:
|
||||
# Doing the sum is not right - overall profit needs to be based on initial capital
|
||||
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
|
||||
starting_balance = self._freqtrade.wallets.get_starting_balance()
|
||||
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
|
||||
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
|
||||
profit_closed_ratio_fromstart = 0
|
||||
profit_all_ratio_fromstart = 0
|
||||
if starting_balance:
|
||||
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
|
||||
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
|
||||
|
||||
profit_all_fiat = self._fiat_converter.convert_amount(
|
||||
profit_all_coin_sum,
|
||||
@@ -455,6 +459,9 @@ class RPC:
|
||||
raise RPCException('Error getting current tickers.')
|
||||
|
||||
self._freqtrade.wallets.update(require_update=False)
|
||||
starting_capital = self._freqtrade.wallets.get_starting_balance()
|
||||
starting_cap_fiat = self._fiat_converter.convert_amount(
|
||||
starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||
|
||||
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
||||
if not balance.total:
|
||||
@@ -490,15 +497,25 @@ class RPC:
|
||||
else:
|
||||
raise RPCException('All balances are zero.')
|
||||
|
||||
symbol = fiat_display_currency
|
||||
value = self._fiat_converter.convert_amount(total, stake_currency,
|
||||
symbol) if self._fiat_converter else 0
|
||||
value = self._fiat_converter.convert_amount(
|
||||
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||
|
||||
starting_capital_ratio = 0.0
|
||||
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
|
||||
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
||||
|
||||
return {
|
||||
'currencies': output,
|
||||
'total': total,
|
||||
'symbol': symbol,
|
||||
'symbol': fiat_display_currency,
|
||||
'value': value,
|
||||
'stake': stake_currency,
|
||||
'starting_capital': starting_capital,
|
||||
'starting_capital_ratio': starting_capital_ratio,
|
||||
'starting_capital_pct': round(starting_capital_ratio * 100, 2),
|
||||
'starting_capital_fiat': starting_cap_fiat,
|
||||
'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
|
||||
'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
|
||||
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
||||
}
|
||||
|
||||
@@ -545,12 +562,12 @@ class RPC:
|
||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
|
||||
if order['side'] == 'buy':
|
||||
fully_canceled = self._freqtrade.handle_cancel_buy(
|
||||
fully_canceled = self._freqtrade.handle_cancel_enter(
|
||||
trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||
|
||||
if order['side'] == 'sell':
|
||||
# Cancel order - so it is placed anew with a fresh price.
|
||||
self._freqtrade.handle_cancel_sell(trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||
|
||||
if not fully_canceled:
|
||||
# Get current rate and execute sell
|
||||
@@ -563,7 +580,7 @@ class RPC:
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('trader is not running')
|
||||
|
||||
with self._freqtrade._sell_lock:
|
||||
with self._freqtrade._exit_lock:
|
||||
if trade_id == 'all':
|
||||
# Execute sell for all open orders
|
||||
for trade in Trade.get_open_trades():
|
||||
@@ -625,7 +642,7 @@ class RPC:
|
||||
Handler for delete <id>.
|
||||
Delete the given trade and close eventually existing open orders.
|
||||
"""
|
||||
with self._freqtrade._sell_lock:
|
||||
with self._freqtrade._exit_lock:
|
||||
c_count = 0
|
||||
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
|
||||
if not trade:
|
||||
@@ -885,3 +902,10 @@ class RPC:
|
||||
'subplots' not in self._freqtrade.strategy.plot_config):
|
||||
self._freqtrade.strategy.plot_config['subplots'] = {}
|
||||
return self._freqtrade.strategy.plot_config
|
||||
|
||||
@staticmethod
|
||||
def _rpc_sysinfo() -> Dict[str, Any]:
|
||||
return {
|
||||
"cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
|
||||
"ram_pct": psutil.virtual_memory().percent
|
||||
}
|
||||
|
@@ -303,6 +303,50 @@ class Telegram(RPCHandler):
|
||||
|
||||
return message
|
||||
|
||||
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
|
||||
|
||||
if msg_type == RPCMessageType.BUY:
|
||||
message = self._format_buy_msg(msg)
|
||||
|
||||
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
||||
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
|
||||
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
||||
"Reason: {reason}.".format(**msg))
|
||||
|
||||
elif msg_type == RPCMessageType.BUY_FILL:
|
||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||
"Buy order for {pair} (#{trade_id}) filled "
|
||||
"for {open_rate}.".format(**msg))
|
||||
elif msg_type == RPCMessageType.SELL_FILL:
|
||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||
"Sell order for {pair} (#{trade_id}) filled "
|
||||
"for {close_rate}.".format(**msg))
|
||||
elif msg_type == RPCMessageType.SELL:
|
||||
message = self._format_sell_msg(msg)
|
||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
||||
message = (
|
||||
"*Protection* triggered due to {reason}. "
|
||||
"`{pair}` will be locked until `{lock_end_time}`."
|
||||
).format(**msg)
|
||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
|
||||
message = (
|
||||
"*Protection* triggered due to {reason}. "
|
||||
"*All pairs* will be locked until `{lock_end_time}`."
|
||||
).format(**msg)
|
||||
elif msg_type == RPCMessageType.STATUS:
|
||||
message = '*Status:* `{status}`'.format(**msg)
|
||||
|
||||
elif msg_type == RPCMessageType.WARNING:
|
||||
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
||||
|
||||
elif msg_type == RPCMessageType.STARTUP:
|
||||
message = '{status}'.format(**msg)
|
||||
|
||||
else:
|
||||
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
|
||||
return message
|
||||
|
||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||
""" Send a message to telegram channel """
|
||||
|
||||
@@ -327,37 +371,7 @@ class Telegram(RPCHandler):
|
||||
# Notification disabled
|
||||
return
|
||||
|
||||
if msg_type == RPCMessageType.BUY:
|
||||
message = self._format_buy_msg(msg)
|
||||
|
||||
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
||||
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
|
||||
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
||||
"Reason: {reason}.".format(**msg))
|
||||
|
||||
elif msg_type == RPCMessageType.BUY_FILL:
|
||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||
"Buy order for {pair} (#{trade_id}) filled "
|
||||
"for {open_rate}.".format(**msg))
|
||||
elif msg_type == RPCMessageType.SELL_FILL:
|
||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||
"Sell order for {pair} (#{trade_id}) filled "
|
||||
"for {close_rate}.".format(**msg))
|
||||
elif msg_type == RPCMessageType.SELL:
|
||||
message = self._format_sell_msg(msg)
|
||||
|
||||
elif msg_type == RPCMessageType.STATUS:
|
||||
message = '*Status:* `{status}`'.format(**msg)
|
||||
|
||||
elif msg_type == RPCMessageType.WARNING:
|
||||
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
||||
|
||||
elif msg_type == RPCMessageType.STARTUP:
|
||||
message = '{status}'.format(**msg)
|
||||
|
||||
else:
|
||||
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
|
||||
message = self.compose_message(msg, msg_type)
|
||||
|
||||
self._send_msg(message, disable_notification=(noti == 'silent'))
|
||||
|
||||
@@ -647,12 +661,15 @@ class Telegram(RPCHandler):
|
||||
|
||||
output = ''
|
||||
if self._config['dry_run']:
|
||||
output += (
|
||||
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"
|
||||
)
|
||||
output += "*Warning:* Simulated balances in Dry Mode.\n"
|
||||
|
||||
output += ("Starting capital: "
|
||||
f"`{result['starting_capital']}` {self._config['stake_currency']}"
|
||||
)
|
||||
output += (f" `{result['starting_capital_fiat']}` "
|
||||
f"{self._config['fiat_display_currency']}.\n"
|
||||
) if result['starting_capital_fiat'] > 0 else '.\n'
|
||||
|
||||
total_dust_balance = 0
|
||||
total_dust_currencies = 0
|
||||
for curr in result['currencies']:
|
||||
@@ -685,9 +702,12 @@ class Telegram(RPCHandler):
|
||||
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
||||
|
||||
output += ("\n*Estimated Value*:\n"
|
||||
f"\t`{result['stake']}: {result['total']: .8f}`\n"
|
||||
f"\t`{result['stake']}: "
|
||||
f"{round_coin_value(result['total'], result['stake'], False)}`"
|
||||
f" `({result['starting_capital_pct']}%)`\n"
|
||||
f"\t`{result['symbol']}: "
|
||||
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
|
||||
f"{round_coin_value(result['value'], result['symbol'], False)}`"
|
||||
f" `({result['starting_capital_fiat_pct']}%)`\n")
|
||||
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
||||
query=update.callback_query)
|
||||
except RPCException as e:
|
||||
|
@@ -3,5 +3,7 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr
|
||||
timeframe_to_prev_date, timeframe_to_seconds)
|
||||
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IntParameter, RealParameter)
|
||||
from freqtrade.strategy.informative_decorator import informative
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open
|
||||
from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute,
|
||||
stoploss_from_open)
|
||||
|
128
freqtrade/strategy/informative_decorator.py
Normal file
128
freqtrade/strategy/informative_decorator.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from typing import Any, Callable, NamedTuple, Optional, Union
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.strategy.strategy_helper import merge_informative_pair
|
||||
|
||||
|
||||
PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame]
|
||||
|
||||
|
||||
class InformativeData(NamedTuple):
|
||||
asset: Optional[str]
|
||||
timeframe: str
|
||||
fmt: Union[str, Callable[[Any], str], None]
|
||||
ffill: bool
|
||||
|
||||
|
||||
def informative(timeframe: str, asset: str = '',
|
||||
fmt: Optional[Union[str, Callable[[Any], str]]] = None,
|
||||
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||
"""
|
||||
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||
define informative indicators.
|
||||
|
||||
Example usage:
|
||||
|
||||
@informative('1h')
|
||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||
current pair.
|
||||
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||
specified, defaults to:
|
||||
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||
* {column}_{timeframe} if asset is not specified.
|
||||
Format string supports these format variables:
|
||||
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||
* {base} - base currency in lower case, for example 'eth'.
|
||||
* {BASE} - same as {base}, except in upper case.
|
||||
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||
* {QUOTE} - same as {quote}, except in upper case.
|
||||
* {column} - name of dataframe column.
|
||||
* {timeframe} - timeframe of informative dataframe.
|
||||
:param ffill: ffill dataframe after merging informative pair.
|
||||
"""
|
||||
_asset = asset
|
||||
_timeframe = timeframe
|
||||
_fmt = fmt
|
||||
_ffill = ffill
|
||||
|
||||
def decorator(fn: PopulateIndicators):
|
||||
informative_pairs = getattr(fn, '_ft_informative', [])
|
||||
informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill))
|
||||
setattr(fn, '_ft_informative', informative_pairs)
|
||||
return fn
|
||||
return decorator
|
||||
|
||||
|
||||
def _format_pair_name(config, pair: str) -> str:
|
||||
return pair.format(stake_currency=config['stake_currency'],
|
||||
stake=config['stake_currency']).upper()
|
||||
|
||||
|
||||
def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict,
|
||||
inf_data: InformativeData,
|
||||
populate_indicators: PopulateIndicators):
|
||||
asset = inf_data.asset or ''
|
||||
timeframe = inf_data.timeframe
|
||||
fmt = inf_data.fmt
|
||||
config = strategy.config
|
||||
|
||||
if asset:
|
||||
# Insert stake currency if needed.
|
||||
asset = _format_pair_name(config, asset)
|
||||
else:
|
||||
# Not specifying an asset will define informative dataframe for current pair.
|
||||
asset = metadata['pair']
|
||||
|
||||
if '/' in asset:
|
||||
base, quote = asset.split('/')
|
||||
else:
|
||||
# When futures are supported this may need reevaluation.
|
||||
# base, quote = asset, ''
|
||||
raise OperationalException('Not implemented.')
|
||||
|
||||
# Default format. This optimizes for the common case: informative pairs using same stake
|
||||
# currency. When quote currency matches stake currency, column name will omit base currency.
|
||||
# This allows easily reconfiguring strategy to use different base currency. In a rare case
|
||||
# where it is desired to keep quote currency in column name at all times user should specify
|
||||
# fmt='{base}_{quote}_{column}_{timeframe}' format or similar.
|
||||
if not fmt:
|
||||
fmt = '{column}_{timeframe}' # Informatives of current pair
|
||||
if inf_data.asset:
|
||||
fmt = '{base}_{quote}_' + fmt # Informatives of other pairs
|
||||
|
||||
inf_metadata = {'pair': asset, 'timeframe': timeframe}
|
||||
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe)
|
||||
inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata)
|
||||
|
||||
formatter: Any = None
|
||||
if callable(fmt):
|
||||
formatter = fmt # A custom user-specified formatter function.
|
||||
else:
|
||||
formatter = fmt.format # A default string formatter.
|
||||
|
||||
fmt_args = {
|
||||
'BASE': base.upper(),
|
||||
'QUOTE': quote.upper(),
|
||||
'base': base.lower(),
|
||||
'quote': quote.lower(),
|
||||
'asset': asset,
|
||||
'timeframe': timeframe,
|
||||
}
|
||||
inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args),
|
||||
inplace=True)
|
||||
|
||||
date_column = formatter(column='date', **fmt_args)
|
||||
if date_column in dataframe.columns:
|
||||
raise OperationalException(f'Duplicate column name {date_column} exists in '
|
||||
f'dataframe! Ensure column names are unique!')
|
||||
dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe,
|
||||
ffill=inf_data.ffill, append_timeframe=False,
|
||||
date_column=date_column)
|
||||
return dataframe
|
@@ -19,6 +19,9 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||
from freqtrade.persistence import PairLocks, Trade
|
||||
from freqtrade.strategy.hyper import HyperStrategyMixin
|
||||
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
||||
_create_and_merge_informative_pair,
|
||||
_format_pair_name)
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
@@ -118,8 +121,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# Class level variables (intentional) containing
|
||||
# the dataprovider (dp) (access to other candles, historic data, ...)
|
||||
# and wallets - access to the current balance.
|
||||
dp: Optional[DataProvider] = None
|
||||
dp: Optional[DataProvider]
|
||||
wallets: Optional[Wallets] = None
|
||||
# Filled from configuration
|
||||
stake_currency: str
|
||||
# container variable for strategy source code
|
||||
__source__: str = ''
|
||||
|
||||
@@ -132,6 +137,24 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||
super().__init__(config)
|
||||
|
||||
# Gather informative pairs from @informative-decorated methods.
|
||||
self._ft_informative: List[Tuple[InformativeData, PopulateIndicators]] = []
|
||||
for attr_name in dir(self.__class__):
|
||||
cls_method = getattr(self.__class__, attr_name)
|
||||
if not callable(cls_method):
|
||||
continue
|
||||
informative_data_list = getattr(cls_method, '_ft_informative', None)
|
||||
if not isinstance(informative_data_list, list):
|
||||
# Type check is required because mocker would return a mock object that evaluates to
|
||||
# True, confusing this code.
|
||||
continue
|
||||
strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe)
|
||||
for informative_data in informative_data_list:
|
||||
if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes:
|
||||
raise OperationalException('Informative timeframe must be equal or higher than '
|
||||
'strategy timeframe!')
|
||||
self._ft_informative.append((informative_data, cls_method))
|
||||
|
||||
@abstractmethod
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
@@ -375,6 +398,23 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# END - Intended to be overridden by strategy
|
||||
###
|
||||
|
||||
def gather_informative_pairs(self) -> ListPairsWithTimeframes:
|
||||
"""
|
||||
Internal method which gathers all informative pairs (user or automatically defined).
|
||||
"""
|
||||
informative_pairs = self.informative_pairs()
|
||||
for inf_data, _ in self._ft_informative:
|
||||
if inf_data.asset:
|
||||
pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe)
|
||||
informative_pairs.append(pair_tf)
|
||||
else:
|
||||
if not self.dp:
|
||||
raise OperationalException('@informative decorator with unspecified asset '
|
||||
'requires DataProvider instance.')
|
||||
for pair in self.dp.current_whitelist():
|
||||
informative_pairs.append((pair, inf_data.timeframe))
|
||||
return list(set(informative_pairs))
|
||||
|
||||
def get_strategy_name(self) -> str:
|
||||
"""
|
||||
Returns strategy class name
|
||||
@@ -777,10 +817,11 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
Does not run advise_buy or advise_sell!
|
||||
Used by optimize operations only, not during dry / live runs.
|
||||
Using .copy() to get a fresh copy of the dataframe for every strategy run.
|
||||
Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show.
|
||||
Has positive effects on memory usage for whatever reason - also when
|
||||
using only one strategy.
|
||||
"""
|
||||
return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair})
|
||||
return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair}).copy()
|
||||
for pair, pair_data in data.items()}
|
||||
|
||||
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
@@ -792,6 +833,12 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
logger.debug(f"Populating indicators for pair {metadata.get('pair')}.")
|
||||
|
||||
# call populate_indicators_Nm() which were tagged with @informative decorator.
|
||||
for inf_data, populate_fn in self._ft_informative:
|
||||
dataframe = _create_and_merge_informative_pair(
|
||||
self, dataframe, metadata, inf_data, populate_fn)
|
||||
|
||||
if self._populate_fun_len == 2:
|
||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||
"the current function headers!", DeprecationWarning)
|
||||
|
@@ -4,7 +4,9 @@ from freqtrade.exchange import timeframe_to_minutes
|
||||
|
||||
|
||||
def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
||||
timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame:
|
||||
timeframe: str, timeframe_inf: str, ffill: bool = True,
|
||||
append_timeframe: bool = True,
|
||||
date_column: str = 'date') -> pd.DataFrame:
|
||||
"""
|
||||
Correctly merge informative samples to the original dataframe, avoiding lookahead bias.
|
||||
|
||||
@@ -24,6 +26,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
||||
:param timeframe: Timeframe of the original pair sample.
|
||||
:param timeframe_inf: Timeframe of the informative pair sample.
|
||||
:param ffill: Forwardfill missing values - optional but usually required
|
||||
:param append_timeframe: Rename columns by appending timeframe.
|
||||
:param date_column: A custom date column name.
|
||||
:return: Merged dataframe
|
||||
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe
|
||||
"""
|
||||
@@ -32,25 +36,29 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
||||
minutes = timeframe_to_minutes(timeframe)
|
||||
if minutes == minutes_inf:
|
||||
# No need to forwardshift if the timeframes are identical
|
||||
informative['date_merge'] = informative["date"]
|
||||
informative['date_merge'] = informative[date_column]
|
||||
elif minutes < minutes_inf:
|
||||
# Subtract "small" timeframe so merging is not delayed by 1 small candle
|
||||
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
|
||||
informative['date_merge'] = (
|
||||
informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm')
|
||||
informative[date_column] + pd.to_timedelta(minutes_inf, 'm') -
|
||||
pd.to_timedelta(minutes, 'm')
|
||||
)
|
||||
else:
|
||||
raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
|
||||
"This would create new rows, and can throw off your regular indicators.")
|
||||
|
||||
# Rename columns to be unique
|
||||
informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns]
|
||||
date_merge = 'date_merge'
|
||||
if append_timeframe:
|
||||
date_merge = f'date_merge_{timeframe_inf}'
|
||||
informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns]
|
||||
|
||||
# Combine the 2 dataframes
|
||||
# all indicators on the informative sample MUST be calculated before this point
|
||||
dataframe = pd.merge(dataframe, informative, left_on='date',
|
||||
right_on=f'date_merge_{timeframe_inf}', how='left')
|
||||
dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1)
|
||||
right_on=date_merge, how='left')
|
||||
dataframe = dataframe.drop(date_merge, axis=1)
|
||||
|
||||
if ffill:
|
||||
dataframe = dataframe.ffill()
|
||||
@@ -83,3 +91,28 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa
|
||||
|
||||
# negative stoploss values indicate the requested stop price is higher than the current price
|
||||
return max(stoploss, 0.0)
|
||||
|
||||
|
||||
def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float:
|
||||
"""
|
||||
Given current price and desired stop price, return a stop loss value that is relative to current
|
||||
price.
|
||||
|
||||
The requested stop can be positive for a stop above the open price, or negative for
|
||||
a stop below the open price. The return value is always >= 0.
|
||||
|
||||
Returns 0 if the resulting stop price would be above the current price.
|
||||
|
||||
:param stop_rate: Stop loss price.
|
||||
:param current_rate: Current asset price.
|
||||
:return: Positive stop loss value relative to current price
|
||||
"""
|
||||
|
||||
# formula is undefined for current_rate 0, return maximum value
|
||||
if current_rate == 0:
|
||||
return 1
|
||||
|
||||
stoploss = 1 - (stop_rate / current_rate)
|
||||
|
||||
# negative stoploss values indicate the requested stop price is higher than the current price
|
||||
return max(stoploss, 0.0)
|
||||
|
@@ -1,3 +1,10 @@
|
||||
{%set volume_pairlist = '{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}' %}
|
||||
{
|
||||
"max_open_trades": {{ max_open_trades }},
|
||||
"stake_currency": "{{ stake_currency }}",
|
||||
@@ -29,7 +36,7 @@
|
||||
},
|
||||
{{ exchange | indent(4) }},
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
{{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }}
|
||||
],
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
|
@@ -1,137 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
|
||||
# --- Do not remove these libs ---
|
||||
from functools import reduce
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
import numpy as np # noqa
|
||||
import pandas as pd # noqa
|
||||
from pandas import DataFrame
|
||||
from skopt.space import Categorical, Dimension, Integer, Real # noqa
|
||||
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||
|
||||
# --------------------------------
|
||||
# Add your lib to import here
|
||||
import talib.abstract as ta # noqa
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
|
||||
|
||||
class {{ hyperopt }}(IHyperOpt):
|
||||
"""
|
||||
This is a Hyperopt template to get you started.
|
||||
|
||||
More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/
|
||||
|
||||
You should:
|
||||
- Add any lib you need to build your hyperopt.
|
||||
|
||||
You must keep:
|
||||
- The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
|
||||
|
||||
The methods roi_space, generate_roi_table and stoploss_space are not required
|
||||
and are provided by default.
|
||||
However, you may override them if you need 'roi' and 'stoploss' spaces that
|
||||
differ from the defaults offered by Freqtrade.
|
||||
Sample implementation of these methods will be copied to `user_data/hyperopts` when
|
||||
creating the user-data directory using `freqtrade create-userdir --userdir user_data`,
|
||||
or is available online under the following URL:
|
||||
https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching buy strategy parameters.
|
||||
"""
|
||||
return [
|
||||
{{ buy_space | indent(12) }}
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Define the buy strategy parameters to be used by Hyperopt.
|
||||
"""
|
||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Buy strategy Hyperopt will build and use.
|
||||
"""
|
||||
conditions = []
|
||||
|
||||
# GUARDS AND TRENDS
|
||||
{{ buy_guards | indent(12) }}
|
||||
|
||||
# TRIGGERS
|
||||
if 'trigger' in params:
|
||||
if params['trigger'] == 'bb_lower':
|
||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||
if params['trigger'] == 'macd_cross_signal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['macd'], dataframe['macdsignal']
|
||||
))
|
||||
if params['trigger'] == 'sar_reversal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['close'], dataframe['sar']
|
||||
))
|
||||
|
||||
# Check that the candle had volume
|
||||
conditions.append(dataframe['volume'] > 0)
|
||||
|
||||
if conditions:
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
return populate_buy_trend
|
||||
|
||||
@staticmethod
|
||||
def sell_indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching sell strategy parameters.
|
||||
"""
|
||||
return [
|
||||
{{ sell_space | indent(12) }}
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Define the sell strategy parameters to be used by Hyperopt.
|
||||
"""
|
||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Sell strategy Hyperopt will build and use.
|
||||
"""
|
||||
conditions = []
|
||||
|
||||
# GUARDS AND TRENDS
|
||||
{{ sell_guards | indent(12) }}
|
||||
|
||||
# TRIGGERS
|
||||
if 'sell-trigger' in params:
|
||||
if params['sell-trigger'] == 'sell-bb_upper':
|
||||
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
|
||||
if params['sell-trigger'] == 'sell-macd_cross_signal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['macdsignal'], dataframe['macd']
|
||||
))
|
||||
if params['sell-trigger'] == 'sell-sar_reversal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['sar'], dataframe['close']
|
||||
))
|
||||
|
||||
# Check that the candle had volume
|
||||
conditions.append(dataframe['volume'] > 0)
|
||||
|
||||
if conditions:
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
'sell'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
return populate_sell_trend
|
||||
|
@@ -1,174 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
# isort: skip_file
|
||||
|
||||
# --- Do not remove these libs ---
|
||||
from functools import reduce
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
import numpy as np # noqa
|
||||
import pandas as pd # noqa
|
||||
from pandas import DataFrame
|
||||
from skopt.space import Categorical, Dimension, Integer, Real # noqa
|
||||
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||
|
||||
# --------------------------------
|
||||
# Add your lib to import here
|
||||
import talib.abstract as ta # noqa
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
|
||||
|
||||
class SampleHyperOpt(IHyperOpt):
|
||||
"""
|
||||
This is a sample Hyperopt to inspire you.
|
||||
|
||||
More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/
|
||||
|
||||
You should:
|
||||
- Rename the class name to some unique name.
|
||||
- Add any methods you want to build your hyperopt.
|
||||
- Add any lib you need to build your hyperopt.
|
||||
|
||||
An easier way to get a new hyperopt file is by using
|
||||
`freqtrade new-hyperopt --hyperopt MyCoolHyperopt`.
|
||||
|
||||
You must keep:
|
||||
- The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
|
||||
|
||||
The methods roi_space, generate_roi_table and stoploss_space are not required
|
||||
and are provided by default.
|
||||
However, you may override them if you need 'roi' and 'stoploss' spaces that
|
||||
differ from the defaults offered by Freqtrade.
|
||||
Sample implementation of these methods will be copied to `user_data/hyperopts` when
|
||||
creating the user-data directory using `freqtrade create-userdir --userdir user_data`,
|
||||
or is available online under the following URL:
|
||||
https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching buy strategy parameters.
|
||||
"""
|
||||
return [
|
||||
Integer(10, 25, name='mfi-value'),
|
||||
Integer(15, 45, name='fastd-value'),
|
||||
Integer(20, 50, name='adx-value'),
|
||||
Integer(20, 40, name='rsi-value'),
|
||||
Categorical([True, False], name='mfi-enabled'),
|
||||
Categorical([True, False], name='fastd-enabled'),
|
||||
Categorical([True, False], name='adx-enabled'),
|
||||
Categorical([True, False], name='rsi-enabled'),
|
||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Define the buy strategy parameters to be used by Hyperopt.
|
||||
"""
|
||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Buy strategy Hyperopt will build and use.
|
||||
"""
|
||||
conditions = []
|
||||
|
||||
# GUARDS AND TRENDS
|
||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
||||
if 'fastd-enabled' in params and params['fastd-enabled']:
|
||||
conditions.append(dataframe['fastd'] < params['fastd-value'])
|
||||
if 'adx-enabled' in params and params['adx-enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||
|
||||
# TRIGGERS
|
||||
if 'trigger' in params:
|
||||
if params['trigger'] == 'bb_lower':
|
||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||
if params['trigger'] == 'macd_cross_signal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['macd'], dataframe['macdsignal']
|
||||
))
|
||||
if params['trigger'] == 'sar_reversal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['close'], dataframe['sar']
|
||||
))
|
||||
|
||||
# Check that volume is not 0
|
||||
conditions.append(dataframe['volume'] > 0)
|
||||
|
||||
if conditions:
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
return populate_buy_trend
|
||||
|
||||
@staticmethod
|
||||
def sell_indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching sell strategy parameters.
|
||||
"""
|
||||
return [
|
||||
Integer(75, 100, name='sell-mfi-value'),
|
||||
Integer(50, 100, name='sell-fastd-value'),
|
||||
Integer(50, 100, name='sell-adx-value'),
|
||||
Integer(60, 100, name='sell-rsi-value'),
|
||||
Categorical([True, False], name='sell-mfi-enabled'),
|
||||
Categorical([True, False], name='sell-fastd-enabled'),
|
||||
Categorical([True, False], name='sell-adx-enabled'),
|
||||
Categorical([True, False], name='sell-rsi-enabled'),
|
||||
Categorical(['sell-bb_upper',
|
||||
'sell-macd_cross_signal',
|
||||
'sell-sar_reversal'], name='sell-trigger')
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Define the sell strategy parameters to be used by Hyperopt.
|
||||
"""
|
||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Sell strategy Hyperopt will build and use.
|
||||
"""
|
||||
conditions = []
|
||||
|
||||
# GUARDS AND TRENDS
|
||||
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
||||
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
||||
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
|
||||
conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
|
||||
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
|
||||
conditions.append(dataframe['adx'] < params['sell-adx-value'])
|
||||
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
|
||||
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
||||
|
||||
# TRIGGERS
|
||||
if 'sell-trigger' in params:
|
||||
if params['sell-trigger'] == 'sell-bb_upper':
|
||||
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
|
||||
if params['sell-trigger'] == 'sell-macd_cross_signal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['macdsignal'], dataframe['macd']
|
||||
))
|
||||
if params['sell-trigger'] == 'sell-sar_reversal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['sar'], dataframe['close']
|
||||
))
|
||||
|
||||
# Check that volume is not 0
|
||||
conditions.append(dataframe['volume'] > 0)
|
||||
|
||||
if conditions:
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
'sell'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
return populate_sell_trend
|
@@ -1,269 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
# isort: skip_file
|
||||
# --- Do not remove these libs ---
|
||||
from functools import reduce
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
import numpy as np # noqa
|
||||
import pandas as pd # noqa
|
||||
from pandas import DataFrame
|
||||
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa
|
||||
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||
|
||||
# --------------------------------
|
||||
# Add your lib to import here
|
||||
import talib.abstract as ta # noqa
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
|
||||
|
||||
class AdvancedSampleHyperOpt(IHyperOpt):
|
||||
"""
|
||||
This is a sample hyperopt to inspire you.
|
||||
Feel free to customize it.
|
||||
|
||||
More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/
|
||||
|
||||
You should:
|
||||
- Rename the class name to some unique name.
|
||||
- Add any methods you want to build your hyperopt.
|
||||
- Add any lib you need to build your hyperopt.
|
||||
|
||||
You must keep:
|
||||
- The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
|
||||
|
||||
The methods roi_space, generate_roi_table and stoploss_space are not required
|
||||
and are provided by default.
|
||||
However, you may override them if you need the
|
||||
'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade.
|
||||
|
||||
This sample illustrates how to override these methods.
|
||||
"""
|
||||
@staticmethod
|
||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
This method can also be loaded from the strategy, if it doesn't exist in the hyperopt class.
|
||||
"""
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
return dataframe
|
||||
|
||||
@staticmethod
|
||||
def indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching buy strategy parameters.
|
||||
"""
|
||||
return [
|
||||
Integer(10, 25, name='mfi-value'),
|
||||
Integer(15, 45, name='fastd-value'),
|
||||
Integer(20, 50, name='adx-value'),
|
||||
Integer(20, 40, name='rsi-value'),
|
||||
Categorical([True, False], name='mfi-enabled'),
|
||||
Categorical([True, False], name='fastd-enabled'),
|
||||
Categorical([True, False], name='adx-enabled'),
|
||||
Categorical([True, False], name='rsi-enabled'),
|
||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Define the buy strategy parameters to be used by hyperopt
|
||||
"""
|
||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Buy strategy Hyperopt will build and use
|
||||
"""
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
||||
if 'fastd-enabled' in params and params['fastd-enabled']:
|
||||
conditions.append(dataframe['fastd'] < params['fastd-value'])
|
||||
if 'adx-enabled' in params and params['adx-enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||
|
||||
# TRIGGERS
|
||||
if 'trigger' in params:
|
||||
if params['trigger'] == 'bb_lower':
|
||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||
if params['trigger'] == 'macd_cross_signal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['macd'], dataframe['macdsignal']
|
||||
))
|
||||
if params['trigger'] == 'sar_reversal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['close'], dataframe['sar']
|
||||
))
|
||||
|
||||
# Check that volume is not 0
|
||||
conditions.append(dataframe['volume'] > 0)
|
||||
|
||||
if conditions:
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
return populate_buy_trend
|
||||
|
||||
@staticmethod
|
||||
def sell_indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching sell strategy parameters.
|
||||
"""
|
||||
return [
|
||||
Integer(75, 100, name='sell-mfi-value'),
|
||||
Integer(50, 100, name='sell-fastd-value'),
|
||||
Integer(50, 100, name='sell-adx-value'),
|
||||
Integer(60, 100, name='sell-rsi-value'),
|
||||
Categorical([True, False], name='sell-mfi-enabled'),
|
||||
Categorical([True, False], name='sell-fastd-enabled'),
|
||||
Categorical([True, False], name='sell-adx-enabled'),
|
||||
Categorical([True, False], name='sell-rsi-enabled'),
|
||||
Categorical(['sell-bb_upper',
|
||||
'sell-macd_cross_signal',
|
||||
'sell-sar_reversal'], name='sell-trigger')
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Define the sell strategy parameters to be used by hyperopt
|
||||
"""
|
||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Sell strategy Hyperopt will build and use
|
||||
"""
|
||||
# print(params)
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
||||
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
||||
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
|
||||
conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
|
||||
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
|
||||
conditions.append(dataframe['adx'] < params['sell-adx-value'])
|
||||
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
|
||||
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
||||
|
||||
# TRIGGERS
|
||||
if 'sell-trigger' in params:
|
||||
if params['sell-trigger'] == 'sell-bb_upper':
|
||||
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
|
||||
if params['sell-trigger'] == 'sell-macd_cross_signal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['macdsignal'], dataframe['macd']
|
||||
))
|
||||
if params['sell-trigger'] == 'sell-sar_reversal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['sar'], dataframe['close']
|
||||
))
|
||||
|
||||
# Check that volume is not 0
|
||||
conditions.append(dataframe['volume'] > 0)
|
||||
|
||||
if conditions:
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
'sell'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
return populate_sell_trend
|
||||
|
||||
@staticmethod
|
||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||
"""
|
||||
Generate the ROI table that will be used by Hyperopt
|
||||
|
||||
This implementation generates the default legacy Freqtrade ROI tables.
|
||||
|
||||
Change it if you need different number of steps in the generated
|
||||
ROI tables or other structure of the ROI tables.
|
||||
|
||||
Please keep it aligned with parameters in the 'roi' optimization
|
||||
hyperspace defined by the roi_space method.
|
||||
"""
|
||||
roi_table = {}
|
||||
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
||||
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
|
||||
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
|
||||
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
|
||||
|
||||
return roi_table
|
||||
|
||||
@staticmethod
|
||||
def roi_space() -> List[Dimension]:
|
||||
"""
|
||||
Values to search for each ROI steps
|
||||
|
||||
Override it if you need some different ranges for the parameters in the
|
||||
'roi' optimization hyperspace.
|
||||
|
||||
Please keep it aligned with the implementation of the
|
||||
generate_roi_table method.
|
||||
"""
|
||||
return [
|
||||
Integer(10, 120, name='roi_t1'),
|
||||
Integer(10, 60, name='roi_t2'),
|
||||
Integer(10, 40, name='roi_t3'),
|
||||
SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'),
|
||||
SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'),
|
||||
SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def stoploss_space() -> List[Dimension]:
|
||||
"""
|
||||
Stoploss Value to search
|
||||
|
||||
Override it if you need some different range for the parameter in the
|
||||
'stoploss' optimization hyperspace.
|
||||
"""
|
||||
return [
|
||||
SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def trailing_space() -> List[Dimension]:
|
||||
"""
|
||||
Create a trailing stoploss space.
|
||||
|
||||
You may override it in your custom Hyperopt class.
|
||||
"""
|
||||
return [
|
||||
# It was decided to always set trailing_stop is to True if the 'trailing' hyperspace
|
||||
# is used. Otherwise hyperopt will vary other parameters that won't have effect if
|
||||
# trailing_stop is set False.
|
||||
# This parameter is included into the hyperspace dimensions rather than assigning
|
||||
# it explicitly in the code in order to have it printed in the results along with
|
||||
# other 'trailing' hyperspace parameters.
|
||||
Categorical([True], name='trailing_stop'),
|
||||
|
||||
SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'),
|
||||
|
||||
# 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive',
|
||||
# so this intermediate parameter is used as the value of the difference between
|
||||
# them. The value of the 'trailing_stop_positive_offset' is constructed in the
|
||||
# generate_trailing_params() method.
|
||||
# This is similar to the hyperspace dimensions used for constructing the ROI tables.
|
||||
SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'),
|
||||
|
||||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
||||
]
|
@@ -2,40 +2,11 @@
|
||||
"name": "{{ exchange_name | lower }}",
|
||||
"key": "{{ exchange_key }}",
|
||||
"secret": "{{ exchange_secret }}",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 200
|
||||
},
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"ALGO/BTC",
|
||||
"ATOM/BTC",
|
||||
"BAT/BTC",
|
||||
"BCH/BTC",
|
||||
"BRD/BTC",
|
||||
"EOS/BTC",
|
||||
"ETH/BTC",
|
||||
"IOTA/BTC",
|
||||
"LINK/BTC",
|
||||
"LTC/BTC",
|
||||
"NEO/BTC",
|
||||
"NXS/BTC",
|
||||
"XMR/BTC",
|
||||
"XRP/BTC",
|
||||
"XTZ/BTC"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/BTC",
|
||||
"BNB/BUSD",
|
||||
"BNB/ETH",
|
||||
"BNB/EUR",
|
||||
"BNB/NGN",
|
||||
"BNB/PAX",
|
||||
"BNB/RUB",
|
||||
"BNB/TRY",
|
||||
"BNB/TUSD",
|
||||
"BNB/USDC",
|
||||
"BNB/USDS",
|
||||
"BNB/USDT"
|
||||
"BNB/.*"
|
||||
]
|
||||
}
|
||||
|
@@ -15,16 +15,6 @@
|
||||
"rateLimit": 500
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"ETH/BTC",
|
||||
"LTC/BTC",
|
||||
"ETC/BTC",
|
||||
"DASH/BTC",
|
||||
"ZEC/BTC",
|
||||
"XLM/BTC",
|
||||
"XRP/BTC",
|
||||
"TRX/BTC",
|
||||
"ADA/BTC",
|
||||
"XMR/BTC"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
]
|
||||
|
@@ -2,10 +2,8 @@
|
||||
"name": "{{ exchange_name | lower }}",
|
||||
"key": "{{ exchange_key }}",
|
||||
"secret": "{{ exchange_secret }}",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true
|
||||
},
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
|
||||
],
|
||||
|
@@ -7,28 +7,10 @@
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 1000
|
||||
// Enable the below for downoading data.
|
||||
//"rateLimit": 3100
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"ADA/EUR",
|
||||
"ATOM/EUR",
|
||||
"BAT/EUR",
|
||||
"BCH/EUR",
|
||||
"BTC/EUR",
|
||||
"DAI/EUR",
|
||||
"DASH/EUR",
|
||||
"EOS/EUR",
|
||||
"ETC/EUR",
|
||||
"ETH/EUR",
|
||||
"LINK/EUR",
|
||||
"LTC/EUR",
|
||||
"QTUM/EUR",
|
||||
"REP/EUR",
|
||||
"WAVES/EUR",
|
||||
"XLM/EUR",
|
||||
"XMR/EUR",
|
||||
"XRP/EUR",
|
||||
"XTZ/EUR",
|
||||
"ZEC/EUR"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
|
||||
|
12
freqtrade/templates/subtemplates/exchange_kucoin.j2
Normal file
12
freqtrade/templates/subtemplates/exchange_kucoin.j2
Normal file
@@ -0,0 +1,12 @@
|
||||
"exchange": {
|
||||
"name": "{{ exchange_name | lower }}",
|
||||
"key": "{{ exchange_key }}",
|
||||
"secret": "{{ exchange_secret }}",
|
||||
"password": "{{ exchange_key_password }}",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
],
|
||||
"pair_blacklist": [
|
||||
]
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
if params.get('mfi-enabled'):
|
||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
||||
if params.get('fastd-enabled'):
|
||||
conditions.append(dataframe['fastd'] < params['fastd-value'])
|
||||
if params.get('adx-enabled'):
|
||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
||||
if params.get('rsi-enabled'):
|
||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
@@ -1,2 +0,0 @@
|
||||
if params.get('rsi-enabled'):
|
||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
@@ -1,9 +0,0 @@
|
||||
Integer(10, 25, name='mfi-value'),
|
||||
Integer(15, 45, name='fastd-value'),
|
||||
Integer(20, 50, name='adx-value'),
|
||||
Integer(20, 40, name='rsi-value'),
|
||||
Categorical([True, False], name='mfi-enabled'),
|
||||
Categorical([True, False], name='fastd-enabled'),
|
||||
Categorical([True, False], name='adx-enabled'),
|
||||
Categorical([True, False], name='rsi-enabled'),
|
||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
@@ -1,3 +0,0 @@
|
||||
Integer(20, 40, name='rsi-value'),
|
||||
Categorical([True, False], name='rsi-enabled'),
|
||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
@@ -1,8 +0,0 @@
|
||||
if params.get('sell-mfi-enabled'):
|
||||
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
||||
if params.get('sell-fastd-enabled'):
|
||||
conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
|
||||
if params.get('sell-adx-enabled'):
|
||||
conditions.append(dataframe['adx'] < params['sell-adx-value'])
|
||||
if params.get('sell-rsi-enabled'):
|
||||
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
@@ -1,2 +0,0 @@
|
||||
if params.get('sell-rsi-enabled'):
|
||||
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
@@ -1,11 +0,0 @@
|
||||
Integer(75, 100, name='sell-mfi-value'),
|
||||
Integer(50, 100, name='sell-fastd-value'),
|
||||
Integer(50, 100, name='sell-adx-value'),
|
||||
Integer(60, 100, name='sell-rsi-value'),
|
||||
Categorical([True, False], name='sell-mfi-enabled'),
|
||||
Categorical([True, False], name='sell-fastd-enabled'),
|
||||
Categorical([True, False], name='sell-adx-enabled'),
|
||||
Categorical([True, False], name='sell-rsi-enabled'),
|
||||
Categorical(['sell-bb_upper',
|
||||
'sell-macd_cross_signal',
|
||||
'sell-sar_reversal'], name='sell-trigger')
|
@@ -1,5 +0,0 @@
|
||||
Integer(60, 100, name='sell-rsi-value'),
|
||||
Categorical([True, False], name='sell-rsi-enabled'),
|
||||
Categorical(['sell-bb_upper',
|
||||
'sell-macd_cross_signal',
|
||||
'sell-sar_reversal'], name='sell-trigger')
|
@@ -32,8 +32,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate:
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
|
||||
current_rate: float, current_profit: float, dataframe: DataFrame,
|
||||
**kwargs) -> float:
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
"""
|
||||
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
||||
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||
@@ -44,14 +43,13 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
|
||||
When not implemented by a strategy, returns the initial stoploss value
|
||||
Only called when use_custom_stoploss is set to True.
|
||||
|
||||
:param pair: Pair that's about to be sold.
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param trade: trade object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New stoploss value, relative to the currentrate
|
||||
:return float: New stoploss value, relative to the current_rate
|
||||
"""
|
||||
return self.stoploss
|
||||
|
||||
|
Reference in New Issue
Block a user