Merge branch 'develop' into data_handler
This commit is contained in:
@@ -7,6 +7,7 @@ Note: Be careful with file-scoped imports in these subfiles.
|
||||
as they are parsed on startup, nothing containing optional modules should be loaded.
|
||||
"""
|
||||
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)
|
||||
from freqtrade.commands.deploy_commands import (start_create_userdir,
|
||||
|
@@ -6,8 +6,8 @@ from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS
|
||||
from freqtrade.constants import DEFAULT_CONFIG
|
||||
|
||||
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
|
||||
|
||||
@@ -30,9 +30,9 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||
|
||||
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||
|
||||
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column"]
|
||||
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"]
|
||||
|
||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"]
|
||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
||||
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||
|
||||
@@ -45,6 +45,8 @@ ARGS_TEST_PAIRLIST = ["config", "quote_currencies", "print_one_column", "list_pa
|
||||
|
||||
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
|
||||
|
||||
ARGS_BUILD_CONFIG = ["config"]
|
||||
|
||||
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||
|
||||
ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"]
|
||||
@@ -62,8 +64,12 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
||||
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
||||
"trade_source", "ticker_interval"]
|
||||
|
||||
ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", "print_colorized",
|
||||
"print_json", "hyperopt_list_no_details"]
|
||||
ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
|
||||
"hyperopt_list_min_trades", "hyperopt_list_max_trades",
|
||||
"hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time",
|
||||
"hyperopt_list_min_avg_profit", "hyperopt_list_max_avg_profit",
|
||||
"hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit",
|
||||
"print_colorized", "print_json", "hyperopt_list_no_details"]
|
||||
|
||||
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
|
||||
"print_json", "hyperopt_show_no_header"]
|
||||
@@ -105,10 +111,23 @@ class Arguments:
|
||||
# Workaround issue in argparse with action='append' and default value
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
# Allow no-config for certain commands (like downloading / plotting)
|
||||
if ('config' in parsed_arg and parsed_arg.config is None and
|
||||
((Path.cwd() / constants.DEFAULT_CONFIG).is_file() or
|
||||
not ('command' in parsed_arg and parsed_arg.command in NO_CONF_REQURIED))):
|
||||
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
||||
if ('config' in parsed_arg and parsed_arg.config is None):
|
||||
conf_required = ('command' in parsed_arg and parsed_arg.command in NO_CONF_REQURIED)
|
||||
|
||||
if 'user_data_dir' in parsed_arg and parsed_arg.user_data_dir is not None:
|
||||
user_dir = parsed_arg.user_data_dir
|
||||
else:
|
||||
# Default case
|
||||
user_dir = 'user_data'
|
||||
# Try loading from "user_data/config.json"
|
||||
cfgfile = Path(user_dir) / DEFAULT_CONFIG
|
||||
if cfgfile.is_file():
|
||||
parsed_arg.config = [str(cfgfile)]
|
||||
else:
|
||||
# Else use "config.json".
|
||||
cfgfile = Path.cwd() / DEFAULT_CONFIG
|
||||
if cfgfile.is_file() or not conf_required:
|
||||
parsed_arg.config = [DEFAULT_CONFIG]
|
||||
|
||||
return parsed_arg
|
||||
|
||||
@@ -141,7 +160,7 @@ class Arguments:
|
||||
start_hyperopt_list, start_hyperopt_show,
|
||||
start_list_exchanges, start_list_hyperopts,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes,
|
||||
start_list_timeframes, start_new_config,
|
||||
start_new_hyperopt, start_new_strategy,
|
||||
start_plot_dataframe, start_plot_profit,
|
||||
start_backtesting, start_hyperopt, start_edge,
|
||||
@@ -185,6 +204,12 @@ class Arguments:
|
||||
create_userdir_cmd.set_defaults(func=start_create_userdir)
|
||||
self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd)
|
||||
|
||||
# add new-config subcommand
|
||||
build_config_cmd = subparsers.add_parser('new-config',
|
||||
help="Create new config")
|
||||
build_config_cmd.set_defaults(func=start_new_config)
|
||||
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
|
||||
|
||||
# add new-strategy subcommand
|
||||
build_strategy_cmd = subparsers.add_parser('new-strategy',
|
||||
help="Create new strategy")
|
||||
|
193
freqtrade/commands/build_config_commands.py
Normal file
193
freqtrade/commands/build_config_commands.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from questionary import Separator, prompt
|
||||
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||
from freqtrade.exchange import available_exchanges, MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.misc import render_template
|
||||
from freqtrade.exceptions import OperationalException
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_is_int(val):
|
||||
try:
|
||||
_ = int(val)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def validate_is_float(val):
|
||||
try:
|
||||
_ = float(val)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def ask_user_overwrite(config_path: Path) -> bool:
|
||||
questions = [
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "overwrite",
|
||||
"message": f"File {config_path} already exists. Overwrite?",
|
||||
"default": False,
|
||||
},
|
||||
]
|
||||
answers = prompt(questions)
|
||||
return answers['overwrite']
|
||||
|
||||
|
||||
def ask_user_config() -> Dict[str, Any]:
|
||||
"""
|
||||
Ask user a few questions to build the configuration.
|
||||
Interactive questions built using https://github.com/tmbo/questionary
|
||||
:returns: Dict with keys to put into template
|
||||
"""
|
||||
questions = [
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "dry_run",
|
||||
"message": "Do you want to enable Dry-run (simulated trades)?",
|
||||
"default": True,
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "stake_currency",
|
||||
"message": "Please insert your stake currency:",
|
||||
"default": 'BTC',
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "stake_amount",
|
||||
"message": "Please insert your stake amount:",
|
||||
"default": "0.01",
|
||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(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)
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "ticker_interval",
|
||||
"message": "Please insert your ticker interval:",
|
||||
"default": "5m",
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "fiat_display_currency",
|
||||
"message": "Please insert your display Currency (for reporting):",
|
||||
"default": 'USD',
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"name": "exchange_name",
|
||||
"message": "Select exchange",
|
||||
"choices": [
|
||||
"binance",
|
||||
"binanceje",
|
||||
"binanceus",
|
||||
"bittrex",
|
||||
"kraken",
|
||||
Separator(),
|
||||
"other",
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "autocomplete",
|
||||
"name": "exchange_name",
|
||||
"message": "Type your exchange name (Must be supported by ccxt)",
|
||||
"choices": available_exchanges(),
|
||||
"when": lambda x: x["exchange_name"] == 'other'
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_key",
|
||||
"message": "Insert Exchange Key",
|
||||
"when": lambda x: not x['dry_run']
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_secret",
|
||||
"message": "Insert Exchange Secret",
|
||||
"when": lambda x: not x['dry_run']
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "telegram",
|
||||
"message": "Do you want to enable Telegram?",
|
||||
"default": False,
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "telegram_token",
|
||||
"message": "Insert Telegram token",
|
||||
"when": lambda x: x['telegram']
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "telegram_chat_id",
|
||||
"message": "Insert Telegram chat id",
|
||||
"when": lambda x: x['telegram']
|
||||
},
|
||||
]
|
||||
answers = prompt(questions)
|
||||
|
||||
if not answers:
|
||||
# Interrupted questionary sessions return an empty dict.
|
||||
raise OperationalException("User interrupted interactive questions.")
|
||||
|
||||
return answers
|
||||
|
||||
|
||||
def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Applies selections to the template and writes the result to config_path
|
||||
:param config_path: Path object for new config file. Should not exist yet
|
||||
:param selecions: Dict containing selections taken by the user.
|
||||
"""
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
try:
|
||||
exchange_template = MAP_EXCHANGE_CHILDCLASS.get(
|
||||
selections['exchange_name'], selections['exchange_name'])
|
||||
|
||||
selections['exchange'] = render_template(
|
||||
templatefile=f"subtemplates/exchange_{exchange_template}.j2",
|
||||
arguments=selections
|
||||
)
|
||||
except TemplateNotFound:
|
||||
selections['exchange'] = render_template(
|
||||
templatefile=f"subtemplates/exchange_generic.j2",
|
||||
arguments=selections
|
||||
)
|
||||
|
||||
config_text = render_template(templatefile='base_config.json.j2',
|
||||
arguments=selections)
|
||||
|
||||
logger.info(f"Writing config to `{config_path}`.")
|
||||
config_path.write_text(config_text)
|
||||
|
||||
|
||||
def start_new_config(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Create a new strategy from a template
|
||||
Asking the user questions to fill out the templateaccordingly.
|
||||
"""
|
||||
|
||||
config_path = Path(args['config'][0])
|
||||
if config_path.exists():
|
||||
overwrite = ask_user_overwrite(config_path)
|
||||
if overwrite:
|
||||
config_path.unlink()
|
||||
else:
|
||||
raise OperationalException(
|
||||
f"Configuration file `{config_path}` already exists. "
|
||||
"Please delete it or use a different configuration file name.")
|
||||
selections = ask_user_config()
|
||||
deploy_new_config(config_path, selections)
|
@@ -59,7 +59,8 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"config": Arg(
|
||||
'-c', '--config',
|
||||
help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). '
|
||||
help=f'Specify configuration file (default: `userdir/{constants.DEFAULT_CONFIG}` '
|
||||
f'or `config.json` whichever exists). '
|
||||
f'Multiple --config options may be used. '
|
||||
f'Can be set to `-` to read config from stdin.',
|
||||
action='append',
|
||||
@@ -422,6 +423,54 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help='Select only best epochs.',
|
||||
action='store_true',
|
||||
),
|
||||
"hyperopt_list_min_trades": Arg(
|
||||
'--min-trades',
|
||||
help='Select epochs with more than INT trades.',
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
),
|
||||
"hyperopt_list_max_trades": Arg(
|
||||
'--max-trades',
|
||||
help='Select epochs with less than INT trades.',
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
),
|
||||
"hyperopt_list_min_avg_time": Arg(
|
||||
'--min-avg-time',
|
||||
help='Select epochs on above average time.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_max_avg_time": Arg(
|
||||
'--max-avg-time',
|
||||
help='Select epochs on under average time.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_min_avg_profit": Arg(
|
||||
'--min-avg-profit',
|
||||
help='Select epochs on above average profit.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_max_avg_profit": Arg(
|
||||
'--max-avg-profit',
|
||||
help='Select epochs on below average profit.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_min_total_profit": Arg(
|
||||
'--min-total-profit',
|
||||
help='Select epochs on above total profit.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_max_total_profit": Arg(
|
||||
'--max-total-profit',
|
||||
help='Select epochs on below total profit.',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"hyperopt_list_no_details": Arg(
|
||||
'--no-details',
|
||||
help='Do not print best epoch details.',
|
||||
|
94
freqtrade/commands/hyperopt_commands.py
Normal file → Executable file
94
freqtrade/commands/hyperopt_commands.py
Normal file → Executable file
@@ -19,13 +19,24 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
only_best = config.get('hyperopt_list_best', False)
|
||||
only_profitable = config.get('hyperopt_list_profitable', False)
|
||||
print_colorized = config.get('print_colorized', False)
|
||||
print_json = config.get('print_json', False)
|
||||
no_details = config.get('hyperopt_list_no_details', False)
|
||||
no_header = False
|
||||
|
||||
filteroptions = {
|
||||
'only_best': config.get('hyperopt_list_best', False),
|
||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None)
|
||||
}
|
||||
|
||||
trials_file = (config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_results.pickle')
|
||||
|
||||
@@ -33,7 +44,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
trials = Hyperopt.load_previous_results(trials_file)
|
||||
total_epochs = len(trials)
|
||||
|
||||
trials = _hyperopt_filter_trials(trials, only_best, only_profitable)
|
||||
trials = _hyperopt_filter_trials(trials, filteroptions)
|
||||
|
||||
# TODO: fetch the interval for epochs to print from the cli option
|
||||
epoch_start, epoch_stop = 0, None
|
||||
@@ -44,7 +55,8 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
try:
|
||||
# Human-friendly indexes used here (starting from 1)
|
||||
for val in trials[epoch_start:epoch_stop]:
|
||||
Hyperopt.print_results_explanation(val, total_epochs, not only_best, print_colorized)
|
||||
Hyperopt.print_results_explanation(val, total_epochs,
|
||||
not filteroptions['only_best'], print_colorized)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print('User interrupted..')
|
||||
@@ -63,8 +75,18 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
only_best = config.get('hyperopt_list_best', False)
|
||||
only_profitable = config.get('hyperopt_list_profitable', False)
|
||||
filteroptions = {
|
||||
'only_best': config.get('hyperopt_list_best', False),
|
||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None)
|
||||
}
|
||||
no_header = config.get('hyperopt_show_no_header', False)
|
||||
|
||||
trials_file = (config['user_data_dir'] /
|
||||
@@ -74,7 +96,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
trials = Hyperopt.load_previous_results(trials_file)
|
||||
total_epochs = len(trials)
|
||||
|
||||
trials = _hyperopt_filter_trials(trials, only_best, only_profitable)
|
||||
trials = _hyperopt_filter_trials(trials, filteroptions)
|
||||
trials_epochs = len(trials)
|
||||
|
||||
n = config.get('hyperopt_show_index', -1)
|
||||
@@ -97,18 +119,66 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
header_str="Epoch details")
|
||||
|
||||
|
||||
def _hyperopt_filter_trials(trials: List, only_best: bool, only_profitable: bool) -> List:
|
||||
def _hyperopt_filter_trials(trials: List, filteroptions: dict) -> List:
|
||||
"""
|
||||
Filter our items from the list of hyperopt results
|
||||
"""
|
||||
if only_best:
|
||||
if filteroptions['only_best']:
|
||||
trials = [x for x in trials if x['is_best']]
|
||||
if only_profitable:
|
||||
if filteroptions['only_profitable']:
|
||||
trials = [x for x in trials if x['results_metrics']['profit'] > 0]
|
||||
if filteroptions['filter_min_trades'] > 0:
|
||||
trials = [
|
||||
x for x in trials
|
||||
if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades']
|
||||
]
|
||||
if filteroptions['filter_max_trades'] > 0:
|
||||
trials = [
|
||||
x for x in trials
|
||||
if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades']
|
||||
]
|
||||
if filteroptions['filter_min_avg_time'] is not None:
|
||||
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
|
||||
trials = [
|
||||
x for x in trials
|
||||
if x['results_metrics']['duration'] > filteroptions['filter_min_avg_time']
|
||||
]
|
||||
if filteroptions['filter_max_avg_time'] is not None:
|
||||
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
|
||||
trials = [
|
||||
x for x in trials
|
||||
if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time']
|
||||
]
|
||||
if filteroptions['filter_min_avg_profit'] is not None:
|
||||
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
|
||||
trials = [
|
||||
x for x in trials
|
||||
if x['results_metrics']['avg_profit']
|
||||
> filteroptions['filter_min_avg_profit']
|
||||
]
|
||||
if filteroptions['filter_max_avg_profit'] is not None:
|
||||
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
|
||||
trials = [
|
||||
x for x in trials
|
||||
if x['results_metrics']['avg_profit']
|
||||
< filteroptions['filter_max_avg_profit']
|
||||
]
|
||||
if filteroptions['filter_min_total_profit'] is not None:
|
||||
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
|
||||
trials = [
|
||||
x for x in trials
|
||||
if x['results_metrics']['profit'] > filteroptions['filter_min_total_profit']
|
||||
]
|
||||
if filteroptions['filter_max_total_profit'] is not None:
|
||||
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
|
||||
trials = [
|
||||
x for x in trials
|
||||
if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit']
|
||||
]
|
||||
|
||||
logger.info(f"{len(trials)} " +
|
||||
("best " if only_best else "") +
|
||||
("profitable " if only_profitable else "") +
|
||||
("best " if filteroptions['only_best'] else "") +
|
||||
("profitable " if filteroptions['only_profitable'] else "") +
|
||||
"epochs found.")
|
||||
|
||||
return trials
|
||||
|
@@ -3,8 +3,10 @@ import logging
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from colorama import init as colorama_init
|
||||
from colorama import Fore, Style
|
||||
import rapidjson
|
||||
from tabulate import tabulate
|
||||
|
||||
@@ -36,6 +38,29 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
||||
print(f"Exchanges available for Freqtrade: {', '.join(exchanges)}")
|
||||
|
||||
|
||||
def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
||||
if print_colorized:
|
||||
colorama_init(autoreset=True)
|
||||
red = Fore.RED
|
||||
yellow = Fore.YELLOW
|
||||
reset = Style.RESET_ALL
|
||||
else:
|
||||
red = ''
|
||||
yellow = ''
|
||||
reset = ''
|
||||
|
||||
names = [s['name'] for s in objs]
|
||||
objss_to_print = [{
|
||||
'name': s['name'] if s['name'] else "--",
|
||||
'location': s['location'].name,
|
||||
'status': (red + "LOAD FAILED" + reset if s['class'] is None
|
||||
else "OK" if names.count(s['name']) == 1
|
||||
else yellow + "DUPLICATE NAME" + reset)
|
||||
} for s in objs]
|
||||
|
||||
print(tabulate(objss_to_print, headers='keys', tablefmt='pipe'))
|
||||
|
||||
|
||||
def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Print files with Strategy custom classes available in the directory
|
||||
@@ -43,15 +68,14 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||
strategies = StrategyResolver.search_all_objects(directory)
|
||||
strategy_objs = StrategyResolver.search_all_objects(directory, not args['print_one_column'])
|
||||
# Sort alphabetically
|
||||
strategies = sorted(strategies, key=lambda x: x['name'])
|
||||
strats_to_print = [{'name': s['name'], 'location': s['location'].name} for s in strategies]
|
||||
strategy_objs = sorted(strategy_objs, key=lambda x: x['name'])
|
||||
|
||||
if args['print_one_column']:
|
||||
print('\n'.join([s['name'] for s in strategies]))
|
||||
print('\n'.join([s['name'] for s in strategy_objs]))
|
||||
else:
|
||||
print(tabulate(strats_to_print, headers='keys', tablefmt='pipe'))
|
||||
_print_objs_tabular(strategy_objs, config.get('print_colorized', False))
|
||||
|
||||
|
||||
def start_list_hyperopts(args: Dict[str, Any]) -> None:
|
||||
@@ -63,15 +87,14 @@ def start_list_hyperopts(args: Dict[str, Any]) -> None:
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
directory = Path(config.get('hyperopt_path', config['user_data_dir'] / USERPATH_HYPEROPTS))
|
||||
hyperopts = HyperOptResolver.search_all_objects(directory)
|
||||
hyperopt_objs = HyperOptResolver.search_all_objects(directory, not args['print_one_column'])
|
||||
# Sort alphabetically
|
||||
hyperopts = sorted(hyperopts, key=lambda x: x['name'])
|
||||
hyperopts_to_print = [{'name': s['name'], 'location': s['location'].name} for s in hyperopts]
|
||||
hyperopt_objs = sorted(hyperopt_objs, key=lambda x: x['name'])
|
||||
|
||||
if args['print_one_column']:
|
||||
print('\n'.join([s['name'] for s in hyperopts]))
|
||||
print('\n'.join([s['name'] for s in hyperopt_objs]))
|
||||
else:
|
||||
print(tabulate(hyperopts_to_print, headers='keys', tablefmt='pipe'))
|
||||
_print_objs_tabular(hyperopt_objs, config.get('print_colorized', False))
|
||||
|
||||
|
||||
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||
|
@@ -310,6 +310,30 @@ class Configuration:
|
||||
self._args_to_config(config, argname='hyperopt_list_profitable',
|
||||
logstring='Parameter --profitable detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_min_trades',
|
||||
logstring='Parameter --min-trades detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_max_trades',
|
||||
logstring='Parameter --max-trades detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_min_avg_time',
|
||||
logstring='Parameter --min-avg-time detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_max_avg_time',
|
||||
logstring='Parameter --max-avg-time detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_min_avg_profit',
|
||||
logstring='Parameter --min-avg-profit detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_max_avg_profit',
|
||||
logstring='Parameter --max-avg-profit detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_min_total_profit',
|
||||
logstring='Parameter --min-total-profit detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_max_total_profit',
|
||||
logstring='Parameter --max-total-profit detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_list_no_details',
|
||||
logstring='Parameter --no-details detected: {}')
|
||||
|
||||
|
@@ -80,7 +80,7 @@ CONF_SCHEMA = {
|
||||
'amend_last_stake_amount': {'type': 'boolean', 'default': False},
|
||||
'last_stake_amount_min_ratio': {
|
||||
'type': 'number', 'minimum': 0.0, 'maximum': 1.0, 'default': 0.5
|
||||
},
|
||||
},
|
||||
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
||||
'dry_run': {'type': 'boolean'},
|
||||
'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET},
|
||||
@@ -193,7 +193,9 @@ CONF_SCHEMA = {
|
||||
'properties': {
|
||||
'enabled': {'type': 'boolean'},
|
||||
'webhookbuy': {'type': 'object'},
|
||||
'webhookbuycancel': {'type': 'object'},
|
||||
'webhooksell': {'type': 'object'},
|
||||
'webhooksellcancel': {'type': 'object'},
|
||||
'webhookstatus': {'type': 'object'},
|
||||
},
|
||||
},
|
||||
|
@@ -1,18 +1,20 @@
|
||||
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS # noqa: F401
|
||||
from freqtrade.exchange.exchange import Exchange # noqa: F401
|
||||
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
|
||||
# flake8: noqa: F401
|
||||
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.exchange import Exchange
|
||||
from freqtrade.exchange.exchange import (get_exchange_bad_reason,
|
||||
is_exchange_bad,
|
||||
is_exchange_known_ccxt,
|
||||
is_exchange_officially_supported,
|
||||
ccxt_exchanges,
|
||||
available_exchanges)
|
||||
from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401
|
||||
from freqtrade.exchange.exchange import (timeframe_to_seconds,
|
||||
timeframe_to_minutes,
|
||||
timeframe_to_msecs,
|
||||
timeframe_to_next_date,
|
||||
timeframe_to_prev_date)
|
||||
from freqtrade.exchange.exchange import (market_is_active, # noqa: F401
|
||||
from freqtrade.exchange.exchange import (market_is_active,
|
||||
symbol_is_pair)
|
||||
from freqtrade.exchange.kraken import Kraken # noqa: F401
|
||||
from freqtrade.exchange.binance import Binance # noqa: F401
|
||||
from freqtrade.exchange.bibox import Bibox # noqa: F401
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.binance import Binance
|
||||
from freqtrade.exchange.bibox import Bibox
|
||||
from freqtrade.exchange.ftx import Ftx
|
||||
|
14
freqtrade/exchange/ftx.py
Normal file
14
freqtrade/exchange/ftx.py
Normal file
@@ -0,0 +1,14 @@
|
||||
""" FTX exchange subclass """
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Ftx(Exchange):
|
||||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 1500,
|
||||
}
|
@@ -234,7 +234,7 @@ class FreqtradeBot:
|
||||
|
||||
return trades_created
|
||||
|
||||
def get_buy_rate(self, pair: str, tick: Dict = None) -> float:
|
||||
def get_buy_rate(self, pair: str, refresh: bool, tick: Dict = None) -> float:
|
||||
"""
|
||||
Calculates bid target between current ask price and last price
|
||||
:return: float: Price
|
||||
@@ -253,7 +253,7 @@ class FreqtradeBot:
|
||||
else:
|
||||
if not tick:
|
||||
logger.info('Using Last Ask / Last Price')
|
||||
ticker = self.exchange.fetch_ticker(pair)
|
||||
ticker = self.exchange.fetch_ticker(pair, refresh)
|
||||
else:
|
||||
ticker = tick
|
||||
if ticker['ask'] < ticker['last']:
|
||||
@@ -404,7 +404,7 @@ class FreqtradeBot:
|
||||
|
||||
stake_amount = self.get_trade_stake_amount(pair)
|
||||
if not stake_amount:
|
||||
logger.debug("Stake amount is 0, ignoring possible trade for {pair}.")
|
||||
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
|
||||
return False
|
||||
|
||||
logger.info(f"Buy signal found: about create a new trade with stake_amount: "
|
||||
@@ -414,10 +414,12 @@ class FreqtradeBot:
|
||||
if ((bid_check_dom.get('enabled', False)) and
|
||||
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
|
||||
if self._check_depth_of_market_buy(pair, bid_check_dom):
|
||||
logger.info(f'Executing Buy for {pair}.')
|
||||
return self.execute_buy(pair, stake_amount)
|
||||
else:
|
||||
return False
|
||||
|
||||
logger.info(f'Executing Buy for {pair}')
|
||||
return self.execute_buy(pair, stake_amount)
|
||||
else:
|
||||
return False
|
||||
@@ -450,7 +452,7 @@ class FreqtradeBot:
|
||||
"""
|
||||
Executes a limit buy for the given pair
|
||||
:param pair: pair for which we want to create a LIMIT_BUY
|
||||
:return: None
|
||||
:return: True if a buy order is created, false if it fails.
|
||||
"""
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
|
||||
@@ -458,7 +460,7 @@ class FreqtradeBot:
|
||||
buy_limit_requested = price
|
||||
else:
|
||||
# Calculate price
|
||||
buy_limit_requested = self.get_buy_rate(pair)
|
||||
buy_limit_requested = self.get_buy_rate(pair, True)
|
||||
|
||||
min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested)
|
||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||
@@ -525,8 +527,6 @@ class FreqtradeBot:
|
||||
ticker_interval=timeframe_to_minutes(self.config['ticker_interval'])
|
||||
)
|
||||
|
||||
self._notify_buy(trade, order_type)
|
||||
|
||||
# Update fees if order is closed
|
||||
if order_status == 'closed':
|
||||
self.update_trade_state(trade, order)
|
||||
@@ -537,6 +537,8 @@ class FreqtradeBot:
|
||||
# Updating wallets
|
||||
self.wallets.update()
|
||||
|
||||
self._notify_buy(trade, order_type)
|
||||
|
||||
return True
|
||||
|
||||
def _notify_buy(self, trade: Trade, order_type: str) -> None:
|
||||
@@ -552,6 +554,32 @@ class FreqtradeBot:
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'amount': trade.amount,
|
||||
'open_date': trade.open_date or datetime.utcnow(),
|
||||
'current_rate': trade.open_rate_requested,
|
||||
}
|
||||
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_buy_cancel(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a buy cancel occured.
|
||||
"""
|
||||
current_rate = self.get_buy_rate(trade.pair, True)
|
||||
|
||||
msg = {
|
||||
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'limit': trade.open_rate,
|
||||
'order_type': order_type,
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'amount': trade.amount,
|
||||
'open_date': trade.open_date,
|
||||
'current_rate': current_rate,
|
||||
}
|
||||
|
||||
# Send the message
|
||||
@@ -751,7 +779,7 @@ class FreqtradeBot:
|
||||
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
||||
# cancelling the current stoploss on exchange first
|
||||
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
|
||||
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) '
|
||||
'in order to add another one ...', order['id'])
|
||||
try:
|
||||
self.exchange.cancel_order(order['id'], trade.pair)
|
||||
@@ -776,8 +804,8 @@ class FreqtradeBot:
|
||||
)
|
||||
|
||||
if should_sell.sell_flag:
|
||||
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
||||
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
||||
logger.info('executed sell, reason: %s', should_sell.sell_type)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -820,41 +848,37 @@ class FreqtradeBot:
|
||||
|
||||
if ((order['side'] == 'buy' and order['status'] == 'canceled')
|
||||
or (self._check_timed_out('buy', order))):
|
||||
|
||||
self.handle_timedout_limit_buy(trade, order)
|
||||
self.wallets.update()
|
||||
order_type = self.strategy.order_types['buy']
|
||||
self._notify_buy_cancel(trade, order_type)
|
||||
|
||||
elif ((order['side'] == 'sell' and order['status'] == 'canceled')
|
||||
or (self._check_timed_out('sell', order))):
|
||||
self.handle_timedout_limit_sell(trade, order)
|
||||
self.wallets.update()
|
||||
|
||||
def handle_buy_order_full_cancel(self, trade: Trade, reason: str) -> None:
|
||||
"""Close trade in database and send message"""
|
||||
Trade.session.delete(trade)
|
||||
Trade.session.flush()
|
||||
logger.info('Buy order %s for %s.', reason, trade)
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': f'Unfilled buy order for {trade.pair} {reason}'
|
||||
})
|
||||
order_type = self.strategy.order_types['sell']
|
||||
self._notify_sell_cancel(trade, order_type)
|
||||
|
||||
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
|
||||
"""
|
||||
Buy timeout - cancel order
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
reason = "cancelled due to timeout"
|
||||
if order['status'] != 'canceled':
|
||||
reason = "cancelled due to timeout"
|
||||
corder = self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||
logger.info('Buy order %s for %s.', reason, trade)
|
||||
else:
|
||||
# Order was cancelled already, so we can reuse the existing dict
|
||||
corder = order
|
||||
reason = "canceled on Exchange"
|
||||
reason = "cancelled on exchange"
|
||||
logger.info('Buy order %s for %s.', reason, trade)
|
||||
|
||||
if corder.get('remaining', order['remaining']) == order['amount']:
|
||||
# if trade is not partially completed, just delete the trade
|
||||
self.handle_buy_order_full_cancel(trade, reason)
|
||||
Trade.session.delete(trade)
|
||||
Trade.session.flush()
|
||||
return True
|
||||
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
@@ -889,24 +913,22 @@ class FreqtradeBot:
|
||||
Sell timeout - cancel order and update trade
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
# if trade is not partially completed, just cancel the trade
|
||||
if order['remaining'] == order['amount']:
|
||||
# if trade is not partially completed, just cancel the trade
|
||||
if order["status"] != "canceled":
|
||||
reason = "due to timeout"
|
||||
reason = "cancelled due to timeout"
|
||||
# if trade is not partially completed, just delete the trade
|
||||
self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||
logger.info('Sell order timeout for %s.', trade)
|
||||
logger.info('Sell order %s for %s.', reason, trade)
|
||||
else:
|
||||
reason = "on exchange"
|
||||
logger.info('Sell order canceled on exchange for %s.', trade)
|
||||
reason = "cancelled on exchange"
|
||||
logger.info('Sell order %s for %s.', reason, trade)
|
||||
|
||||
trade.close_rate = None
|
||||
trade.close_profit = None
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': f'Unfilled sell order for {trade.pair} cancelled {reason}'
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
@@ -938,13 +960,13 @@ class FreqtradeBot:
|
||||
raise DependencyException(
|
||||
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||
|
||||
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None:
|
||||
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> bool:
|
||||
"""
|
||||
Executes a limit sell for the given trade and limit
|
||||
:param trade: Trade instance
|
||||
:param limit: limit rate for the sell order
|
||||
:param sellreason: Reason the sell was triggered
|
||||
:return: None
|
||||
:return: True if it succeeds (supported) False (not supported)
|
||||
"""
|
||||
sell_type = 'sell'
|
||||
if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||
@@ -965,7 +987,7 @@ class FreqtradeBot:
|
||||
|
||||
order_type = self.strategy.order_types[sell_type]
|
||||
if sell_reason == SellType.EMERGENCY_SELL:
|
||||
# Emergencysells (default to market!)
|
||||
# Emergency sells (default to market!)
|
||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||
|
||||
amount = self._safe_sell_amount(trade.pair, trade.amount)
|
||||
@@ -990,6 +1012,8 @@ class FreqtradeBot:
|
||||
|
||||
self._notify_sell(trade, order_type)
|
||||
|
||||
return True
|
||||
|
||||
def _notify_sell(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell occured.
|
||||
@@ -1006,7 +1030,7 @@ class FreqtradeBot:
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
'limit': trade.close_rate_requested,
|
||||
'limit': profit_rate,
|
||||
'order_type': order_type,
|
||||
'amount': trade.amount,
|
||||
'open_rate': trade.open_rate,
|
||||
@@ -1017,6 +1041,44 @@ class FreqtradeBot:
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.utcnow(),
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
}
|
||||
|
||||
if 'fiat_display_currency' in self.config:
|
||||
msg.update({
|
||||
'fiat_currency': self.config['fiat_display_currency'],
|
||||
})
|
||||
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_sell_cancel(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell cancel occured.
|
||||
"""
|
||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||
current_rate = self.get_sell_rate(trade.pair, True)
|
||||
profit_percent = trade.calc_profit_ratio(profit_rate)
|
||||
gain = "profit" if profit_percent > 0 else "loss"
|
||||
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
'limit': profit_rate,
|
||||
'order_type': order_type,
|
||||
'amount': trade.amount,
|
||||
'open_rate': trade.open_rate,
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
'profit_percent': profit_percent,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
}
|
||||
|
||||
if 'fiat_display_currency' in self.config:
|
||||
|
@@ -38,8 +38,8 @@ def main(sysargv: List[str] = None) -> None:
|
||||
# No subcommand was issued.
|
||||
raise OperationalException(
|
||||
"Usage of Freqtrade requires a subcommand to be specified.\n"
|
||||
"To have the previous behavior (bot executing trades in live/dry-run modes, "
|
||||
"depending on the value of the `dry_run` setting in the config), run freqtrade "
|
||||
"To have the bot executing trades in live/dry-run modes, "
|
||||
"depending on the value of the `dry_run` setting in the config, run Freqtrade "
|
||||
"as `freqtrade trade [options...]`.\n"
|
||||
"To see the full list of options available, please use "
|
||||
"`freqtrade --help` or `freqtrade <command> --help`."
|
||||
|
@@ -147,5 +147,4 @@ def render_template(templatefile: str, arguments: dict = {}) -> str:
|
||||
autoescape=select_autoescape(['html', 'xml'])
|
||||
)
|
||||
template = env.get_template(templatefile)
|
||||
|
||||
return template.render(**arguments)
|
||||
|
@@ -39,7 +39,8 @@ class SharpeHyperOptLossDaily(IHyperOptLoss):
|
||||
results['profit_percent'] - slippage_per_trade_ratio
|
||||
|
||||
# create the index within the min_date and end max_date
|
||||
t_index = date_range(start=min_date, end=max_date, freq=resample_freq)
|
||||
t_index = date_range(start=min_date, end=max_date, freq=resample_freq,
|
||||
normalize=True)
|
||||
|
||||
sum_daily = (
|
||||
results.resample(resample_freq, on='close_time').agg(
|
||||
|
@@ -7,7 +7,7 @@ import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
@@ -40,12 +40,14 @@ class IResolver:
|
||||
return abs_paths
|
||||
|
||||
@classmethod
|
||||
def _get_valid_object(cls, module_path: Path,
|
||||
object_name: Optional[str]) -> Generator[Any, None, None]:
|
||||
def _get_valid_object(cls, module_path: Path, object_name: Optional[str],
|
||||
enum_failed: bool = False) -> Iterator[Any]:
|
||||
"""
|
||||
Generator returning objects with matching object_type and object_name in the path given.
|
||||
:param module_path: absolute path to the module
|
||||
:param object_name: Class name of the object
|
||||
:param enum_failed: If True, will return None for modules which fail.
|
||||
Otherwise, failing modules are skipped.
|
||||
:return: generator containing matching objects
|
||||
"""
|
||||
|
||||
@@ -58,6 +60,8 @@ class IResolver:
|
||||
except (ModuleNotFoundError, SyntaxError) as err:
|
||||
# Catch errors in case a specific module is not installed
|
||||
logger.warning(f"Could not import {module_path} due to '{err}'")
|
||||
if enum_failed:
|
||||
return iter([None])
|
||||
|
||||
valid_objects_gen = (
|
||||
obj for name, obj in inspect.getmembers(module, inspect.isclass)
|
||||
@@ -136,10 +140,13 @@ class IResolver:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def search_all_objects(cls, directory: Path) -> List[Dict[str, Any]]:
|
||||
def search_all_objects(cls, directory: Path,
|
||||
enum_failed: bool) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Searches a directory for valid objects
|
||||
:param directory: Path to search
|
||||
:param enum_failed: If True, will return None for modules which fail.
|
||||
Otherwise, failing modules are skipped.
|
||||
:return: List of dicts containing 'name', 'class' and 'location' entires
|
||||
"""
|
||||
logger.debug(f"Searching for {cls.object_type.__name__} '{directory}'")
|
||||
@@ -151,9 +158,10 @@ class IResolver:
|
||||
continue
|
||||
module_path = entry.resolve()
|
||||
logger.debug(f"Path {module_path}")
|
||||
for obj in cls._get_valid_object(module_path, object_name=None):
|
||||
for obj in cls._get_valid_object(module_path, object_name=None,
|
||||
enum_failed=enum_failed):
|
||||
objects.append(
|
||||
{'name': obj.__name__,
|
||||
{'name': obj.__name__ if obj is not None else '',
|
||||
'class': obj,
|
||||
'location': entry,
|
||||
})
|
||||
|
@@ -26,7 +26,9 @@ class RPCMessageType(Enum):
|
||||
WARNING_NOTIFICATION = 'warning'
|
||||
CUSTOM_NOTIFICATION = 'custom'
|
||||
BUY_NOTIFICATION = 'buy'
|
||||
BUY_CANCEL_NOTIFICATION = 'buy_cancel'
|
||||
SELL_NOTIFICATION = 'sell'
|
||||
SELL_CANCEL_NOTIFICATION = 'sell_cancel'
|
||||
|
||||
def __repr__(self):
|
||||
return self.value
|
||||
@@ -39,6 +41,7 @@ class RPCException(Exception):
|
||||
|
||||
raise RPCException('*Status:* `no active trade`')
|
||||
"""
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(self)
|
||||
self.message = message
|
||||
@@ -157,15 +160,17 @@ class RPC:
|
||||
profit_str = f'{trade_perc:.2f}%'
|
||||
if self._fiat_converter:
|
||||
fiat_profit = self._fiat_converter.convert_amount(
|
||||
trade_profit,
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
)
|
||||
trade_profit,
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
)
|
||||
if fiat_profit and not isnan(fiat_profit):
|
||||
profit_str += f" ({fiat_profit:.2f})"
|
||||
trades_list.append([
|
||||
trade.id,
|
||||
trade.pair,
|
||||
trade.pair + ('*' if (trade.open_order_id is not None
|
||||
and trade.close_rate_requested is None) else '')
|
||||
+ ('**' if (trade.close_rate_requested is not None) else ''),
|
||||
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
||||
profit_str
|
||||
])
|
||||
|
@@ -134,13 +134,18 @@ class Telegram(RPC):
|
||||
msg['stake_amount_fiat'] = 0
|
||||
|
||||
message = ("*{exchange}:* Buying {pair}\n"
|
||||
"at rate `{limit:.8f}\n"
|
||||
"({stake_amount:.6f} {stake_currency}").format(**msg)
|
||||
"*Amount:* `{amount:.8f}`\n"
|
||||
"*Open Rate:* `{limit:.8f}`\n"
|
||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||
"*Total:* `({stake_amount:.6f} {stake_currency}").format(**msg)
|
||||
|
||||
if msg.get('fiat_currency', None):
|
||||
message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(**msg)
|
||||
message += ", {stake_amount_fiat:.3f} {fiat_currency}".format(**msg)
|
||||
message += ")`"
|
||||
|
||||
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
|
||||
message = "*{exchange}:* Cancelling Open Buy Order for {pair}".format(**msg)
|
||||
|
||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||
msg['amount'] = round(msg['amount'], 8)
|
||||
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
|
||||
@@ -149,10 +154,10 @@ class Telegram(RPC):
|
||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
||||
|
||||
message = ("*{exchange}:* Selling {pair}\n"
|
||||
"*Rate:* `{limit:.8f}`\n"
|
||||
"*Amount:* `{amount:.8f}`\n"
|
||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||
"*Close Rate:* `{limit:.8f}`\n"
|
||||
"*Sell Reason:* `{sell_reason}`\n"
|
||||
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
|
||||
"*Profit:* `{profit_percent:.2f}%`").format(**msg)
|
||||
@@ -163,8 +168,11 @@ class Telegram(RPC):
|
||||
and self._fiat_converter):
|
||||
msg['profit_fiat'] = self._fiat_converter.convert_amount(
|
||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
message += ('` ({gain}: {profit_amount:.8f} {stake_currency}`'
|
||||
'` / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
||||
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
|
||||
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
||||
|
||||
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
||||
message = "*{exchange}:* Cancelling Open Sell Order for {pair}".format(**msg)
|
||||
|
||||
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
||||
message = '*Status:* `{status}`'.format(**msg)
|
||||
@@ -553,6 +561,8 @@ class Telegram(RPC):
|
||||
"*/stop:* `Stops the trader`\n" \
|
||||
"*/status [table]:* `Lists all open trades`\n" \
|
||||
" *table :* `will display trades in a table`\n" \
|
||||
" `pending buy orders are marked with an asterisk (*)`\n" \
|
||||
" `pending sell orders are marked with a double asterisk (**)`\n" \
|
||||
"*/profit:* `Lists cumulative profit from all finished trades`\n" \
|
||||
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " \
|
||||
"regardless of profit`\n" \
|
||||
|
@@ -41,8 +41,12 @@ class Webhook(RPC):
|
||||
|
||||
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhookbuy', None)
|
||||
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhookbuycancel', None)
|
||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhooksell', None)
|
||||
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhooksellcancel', None)
|
||||
elif msg['type'] in(RPCMessageType.STATUS_NOTIFICATION,
|
||||
RPCMessageType.CUSTOM_NOTIFICATION,
|
||||
RPCMessageType.WARNING_NOTIFICATION):
|
||||
|
58
freqtrade/templates/base_config.json.j2
Normal file
58
freqtrade/templates/base_config.json.j2
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"max_open_trades": {{ max_open_trades }},
|
||||
"stake_currency": "{{ stake_currency }}",
|
||||
"stake_amount": {{ stake_amount }},
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "{{ fiat_display_currency }}",
|
||||
"ticker_interval": "{{ ticker_interval }}",
|
||||
"dry_run": {{ dry_run | lower }},
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0,
|
||||
"use_order_book": false,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"ask_strategy": {
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 9,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
},
|
||||
{{ exchange | indent(4) }},
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
],
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
"calculate_since_number_of_days": 7,
|
||||
"allowed_risk": 0.01,
|
||||
"stoploss_range_min": -0.01,
|
||||
"stoploss_range_max": -0.1,
|
||||
"stoploss_range_step": -0.01,
|
||||
"minimum_winrate": 0.60,
|
||||
"minimum_expectancy": 0.20,
|
||||
"min_trade_number": 10,
|
||||
"max_trade_duration_minute": 1440,
|
||||
"remove_pumps": false
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": {{ telegram | lower }},
|
||||
"token": "{{ telegram_token }}",
|
||||
"chat_id": "{{ telegram_chat_id }}"
|
||||
},
|
||||
"initial_state": "running",
|
||||
"forcebuy_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
}
|
||||
}
|
@@ -6,7 +6,8 @@
|
||||
"source": [
|
||||
"# Strategy analysis example\n",
|
||||
"\n",
|
||||
"Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data."
|
||||
"Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data.\n",
|
||||
"The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -23,18 +24,21 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pathlib import Path\n",
|
||||
"from freqtrade.configuration import Configuration\n",
|
||||
"\n",
|
||||
"# Customize these according to your needs.\n",
|
||||
"\n",
|
||||
"# Initialize empty configuration object\n",
|
||||
"config = Configuration.from_files([])\n",
|
||||
"# Optionally, use existing configuration file\n",
|
||||
"# config = Configuration.from_files([\"config.json\"])\n",
|
||||
"\n",
|
||||
"# Define some constants\n",
|
||||
"timeframe = \"5m\"\n",
|
||||
"config[\"ticker_interval\"] = \"5m\"\n",
|
||||
"# Name of the strategy class\n",
|
||||
"strategy_name = 'SampleStrategy'\n",
|
||||
"# Path to user data\n",
|
||||
"user_data_dir = Path('user_data')\n",
|
||||
"# Location of the strategy\n",
|
||||
"strategy_location = user_data_dir / 'strategies'\n",
|
||||
"config[\"strategy\"] = \"SampleStrategy\"\n",
|
||||
"# Location of the data\n",
|
||||
"data_location = Path(user_data_dir, 'data', 'binance')\n",
|
||||
"data_location = Path(config['user_data_dir'], 'data', 'binance')\n",
|
||||
"# Pair to analyze - Only use one pair here\n",
|
||||
"pair = \"BTC_USDT\""
|
||||
]
|
||||
@@ -49,7 +53,7 @@
|
||||
"from freqtrade.data.history import load_pair_history\n",
|
||||
"\n",
|
||||
"candles = load_pair_history(datadir=data_location,\n",
|
||||
" timeframe=timeframe,\n",
|
||||
" timeframe=config[\"ticker_interval\"],\n",
|
||||
" pair=pair)\n",
|
||||
"\n",
|
||||
"# Confirm success\n",
|
||||
@@ -73,9 +77,7 @@
|
||||
"source": [
|
||||
"# Load strategy using values set above\n",
|
||||
"from freqtrade.resolvers import StrategyResolver\n",
|
||||
"strategy = StrategyResolver.load_strategy({'strategy': strategy_name,\n",
|
||||
" 'user_data_dir': user_data_dir,\n",
|
||||
" 'strategy_path': strategy_location})\n",
|
||||
"strategy = StrategyResolver.load_strategy(config)\n",
|
||||
"\n",
|
||||
"# Generate buy/sell signals using strategy\n",
|
||||
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",
|
||||
@@ -137,7 +139,7 @@
|
||||
"from freqtrade.data.btanalysis import load_backtest_data\n",
|
||||
"\n",
|
||||
"# Load backtest results\n",
|
||||
"trades = load_backtest_data(user_data_dir / \"backtest_results/backtest-result.json\")\n",
|
||||
"trades = load_backtest_data(config[\"user_data_dir\"] / \"backtest_results/backtest-result.json\")\n",
|
||||
"\n",
|
||||
"# Show value-counts per pair\n",
|
||||
"trades.groupby(\"pair\")[\"sell_reason\"].value_counts()"
|
||||
|
41
freqtrade/templates/subtemplates/exchange_binance.j2
Normal file
41
freqtrade/templates/subtemplates/exchange_binance.j2
Normal file
@@ -0,0 +1,41 @@
|
||||
"exchange": {
|
||||
"name": "{{ exchange_name | lower }}",
|
||||
"key": "{{ exchange_key }}",
|
||||
"secret": "{{ exchange_secret }}",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 200
|
||||
},
|
||||
"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",
|
||||
]
|
||||
}
|
31
freqtrade/templates/subtemplates/exchange_bittrex.j2
Normal file
31
freqtrade/templates/subtemplates/exchange_bittrex.j2
Normal file
@@ -0,0 +1,31 @@
|
||||
"order_types": {
|
||||
"buy": "limit",
|
||||
"sell": "limit",
|
||||
"emergencysell": "limit",
|
||||
"stoploss": "limit",
|
||||
"stoploss_on_exchange": false
|
||||
},
|
||||
"exchange": {
|
||||
"name": "{{ exchange_name | lower }}",
|
||||
"key": "{{ exchange_key }}",
|
||||
"secret": "{{ exchange_secret }}",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"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": [
|
||||
]
|
||||
}
|
15
freqtrade/templates/subtemplates/exchange_generic.j2
Normal file
15
freqtrade/templates/subtemplates/exchange_generic.j2
Normal file
@@ -0,0 +1,15 @@
|
||||
"exchange": {
|
||||
"name": "{{ exchange_name | lower }}",
|
||||
"key": "{{ exchange_key }}",
|
||||
"secret": "{{ exchange_secret }}",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true
|
||||
},
|
||||
"pair_whitelist": [
|
||||
|
||||
],
|
||||
"pair_blacklist": [
|
||||
|
||||
]
|
||||
}
|
36
freqtrade/templates/subtemplates/exchange_kraken.j2
Normal file
36
freqtrade/templates/subtemplates/exchange_kraken.j2
Normal file
@@ -0,0 +1,36 @@
|
||||
"download_trades": true,
|
||||
"exchange": {
|
||||
"name": "kraken",
|
||||
"key": "{{ exchange_key }}",
|
||||
"secret": "{{ exchange_secret }}",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 1000
|
||||
},
|
||||
"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": [
|
||||
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user