Merge branch 'develop' into sortino_hyperopt_loss

This commit is contained in:
Yazeed Al Oyoun
2020-02-19 02:37:25 +01:00
committed by GitHub
80 changed files with 2005 additions and 491 deletions

View File

@@ -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_download_data
from freqtrade.commands.deploy_commands import (start_create_userdir,
start_new_hyperopt,

View File

@@ -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"]
@@ -59,8 +61,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"]
@@ -101,10 +107,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
@@ -136,7 +155,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,
@@ -180,6 +199,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")

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

View File

@@ -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',
@@ -399,6 +400,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.',

View File

@@ -37,7 +37,12 @@ def start_download_data(args: Dict[str, Any]) -> None:
pairs_not_available: List[str] = []
# Init exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
# Manual validations of relevant settings
exchange.validate_pairs(config['pairs'])
for timeframe in config['timeframes']:
exchange.validate_timeframes(timeframe)
try:
if config.get('download_trades'):

94
freqtrade/commands/hyperopt_commands.py Normal file → Executable file
View 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

View File

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

View File

@@ -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: {}')

View File

@@ -78,7 +78,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},
@@ -191,7 +191,9 @@ CONF_SCHEMA = {
'properties': {
'enabled': {'type': 'boolean'},
'webhookbuy': {'type': 'object'},
'webhookbuycancel': {'type': 'object'},
'webhooksell': {'type': 'object'},
'webhooksellcancel': {'type': 'object'},
'webhookstatus': {'type': 'object'},
},
},

View File

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

View File

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

View File

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

View File

@@ -139,5 +139,4 @@ def render_template(templatefile: str, arguments: dict = {}) -> str:
autoescape=select_autoescape(['html', 'xml'])
)
template = env.get_template(templatefile)
return template.render(**arguments)

View File

@@ -207,7 +207,7 @@ class IHyperOpt(ABC):
# 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.
# This is similar to the hyperspace dimensions used for constructing the ROI tables.
Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'),
Categorical([True, False], name='trailing_only_offset_is_reached'),

View File

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

View File

@@ -21,13 +21,14 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
tabular_data = []
headers = [
'Pair',
'Buy Count',
'Buys',
'Avg Profit %',
'Cum Profit %',
f'Tot Profit {stake_currency}',
'Tot Profit %',
'Avg Duration',
'Wins',
'Draws',
'Losses'
]
for pair in data:
@@ -45,6 +46,7 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
str(timedelta(
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
len(result[result.profit_abs > 0]),
len(result[result.profit_abs == 0]),
len(result[result.profit_abs < 0])
])
@@ -59,6 +61,7 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]),
len(results[results.profit_abs == 0]),
len(results[results.profit_abs < 0])
])
# Ignore type as floatfmt does allow tuples but mypy does not know that
@@ -78,8 +81,9 @@ def generate_text_table_sell_reason(
tabular_data = []
headers = [
"Sell Reason",
"Sell Count",
"Sells",
"Wins",
"Draws",
"Losses",
"Avg Profit %",
"Cum Profit %",
@@ -88,7 +92,8 @@ def generate_text_table_sell_reason(
]
for reason, count in results['sell_reason'].value_counts().iteritems():
result = results.loc[results['sell_reason'] == reason]
profit = len(result[result['profit_abs'] >= 0])
wins = len(result[result['profit_abs'] > 0])
draws = len(result[result['profit_abs'] == 0])
loss = len(result[result['profit_abs'] < 0])
profit_mean = round(result['profit_percent'].mean() * 100.0, 2)
profit_sum = round(result["profit_percent"].sum() * 100.0, 2)
@@ -98,7 +103,8 @@ def generate_text_table_sell_reason(
[
reason.value,
count,
profit,
wins,
draws,
loss,
profit_mean,
profit_sum,
@@ -121,9 +127,9 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
tabular_data = []
headers = ['Strategy', 'Buy Count', 'Avg Profit %', 'Cum Profit %',
headers = ['Strategy', 'Buys', 'Avg Profit %', 'Cum Profit %',
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
'Wins', 'Losses']
'Wins', 'Draws', 'Losses']
for strategy, results in all_results.items():
tabular_data.append([
strategy,
@@ -135,6 +141,7 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]),
len(results[results.profit_abs == 0]),
len(results[results.profit_abs < 0])
])
# Ignore type as floatfmt does allow tuples but mypy does not know that
@@ -146,9 +153,9 @@ def generate_edge_table(results: dict) -> str:
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d')
tabular_data = []
headers = ['pair', 'stoploss', 'win rate', 'risk reward ratio',
'required risk reward', 'expectancy', 'total number of trades',
'average duration (min)']
headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
'Required Risk Reward', 'Expectancy', 'Total Number of Trades',
'Average Duration (min)']
for result in results.items():
if result[1].nb_trades > 0:

View File

@@ -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,10 +60,13 @@ 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)
if (object_name is None or object_name == name) and cls.object_type in obj.__bases__
if ((object_name is None or object_name == name) and
issubclass(obj, cls.object_type) and obj is not cls.object_type)
)
return valid_objects_gen
@@ -135,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}'")
@@ -150,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,
})

View File

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

View File

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

View File

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

View 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
}
}

View File

@@ -230,7 +230,7 @@ class AdvancedSampleHyperOpt(IHyperOpt):
'stoploss' optimization hyperspace.
"""
return [
Real(-0.5, -0.02, name='stoploss'),
Real(-0.35, -0.02, name='stoploss'),
]
@staticmethod
@@ -249,8 +249,15 @@ class AdvancedSampleHyperOpt(IHyperOpt):
# other 'trailing' hyperspace parameters.
Categorical([True], name='trailing_stop'),
Real(0.02, 0.35, name='trailing_stop_positive'),
Real(0.01, 0.1, name='trailing_stop_positive_offset'),
Real(0.01, 0.35, 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.
Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'),
Categorical([True, False], name='trailing_only_offset_is_reached'),
]

View File

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

View 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",
]
}

View 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": [
]
}

View 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": [
]
}

View 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": [
]
}