Merge branch 'feat/short' into funding-fee-dry-run
This commit is contained in:
@@ -16,7 +16,8 @@ from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hype
|
||||
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets,
|
||||
start_list_strategies, start_list_timeframes,
|
||||
start_show_trades)
|
||||
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
||||
from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show,
|
||||
start_edge, start_hyperopt)
|
||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||
from freqtrade.commands.trade_commands import start_trading
|
||||
|
@@ -41,6 +41,8 @@ ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"]
|
||||
|
||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
||||
|
||||
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"]
|
||||
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||
|
||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||
@@ -94,7 +96,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
||||
|
||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||
"hyperopt-list", "hyperopt-show",
|
||||
"hyperopt-list", "hyperopt-show", "backtest-filter",
|
||||
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||
@@ -173,7 +175,8 @@ class Arguments:
|
||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||
self._build_args(optionlist=['version'], parser=self.parser)
|
||||
|
||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_convert_trades,
|
||||
from freqtrade.commands import (start_backtesting, start_backtesting_show,
|
||||
start_convert_data, start_convert_trades,
|
||||
start_create_userdir, start_download_data, start_edge,
|
||||
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
||||
start_install_ui, start_list_data, start_list_exchanges,
|
||||
@@ -264,6 +267,15 @@ class Arguments:
|
||||
backtesting_cmd.set_defaults(func=start_backtesting)
|
||||
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
|
||||
|
||||
# Add backtesting-show subcommand
|
||||
backtesting_show_cmd = subparsers.add_parser(
|
||||
'backtesting-show',
|
||||
help='Show past Backtest results',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
|
||||
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
|
||||
|
||||
# Add edge subcommand
|
||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
|
||||
parents=[_common_parser, _strategy_parser])
|
||||
|
@@ -83,11 +83,19 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
if val == UNLIMITED_STAKE_AMOUNT
|
||||
else val
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"name": "timeframe_in_config",
|
||||
"message": "Tim",
|
||||
"choices": ["Have the strategy define timeframe.", "Override in configuration."]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "timeframe",
|
||||
"message": "Please insert your desired timeframe (e.g. 5m):",
|
||||
"default": "5m",
|
||||
"when": lambda x: x["timeframe_in_config"] == 'Override in configuration.'
|
||||
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
@@ -107,6 +115,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"ftx",
|
||||
"kucoin",
|
||||
"gateio",
|
||||
"okex",
|
||||
Separator(),
|
||||
"other",
|
||||
],
|
||||
@@ -134,7 +143,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"type": "password",
|
||||
"name": "exchange_key_password",
|
||||
"message": "Insert Exchange API Key password",
|
||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] == 'kucoin'
|
||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okex')
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
|
@@ -152,6 +152,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
action='store_false',
|
||||
default=True,
|
||||
),
|
||||
"backtest_show_pair_list": Arg(
|
||||
'--show-pair-list',
|
||||
help='Show backtesting pairlist sorted by profit.',
|
||||
action='store_true',
|
||||
default=False,
|
||||
),
|
||||
"enable_protections": Arg(
|
||||
'--enable-protections', '--enableprotections',
|
||||
help='Enable protections for backtesting.'
|
||||
|
@@ -54,6 +54,22 @@ def start_backtesting(args: Dict[str, Any]) -> None:
|
||||
backtesting.start()
|
||||
|
||||
|
||||
def start_backtesting_show(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Show previous backtest result
|
||||
"""
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
from freqtrade.data.btanalysis import load_backtest_stats
|
||||
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
|
||||
|
||||
results = load_backtest_stats(config['exportfilename'])
|
||||
|
||||
show_backtest_results(config, results)
|
||||
show_sorted_pairlist(config, results)
|
||||
|
||||
|
||||
def start_hyperopt(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Start hyperopt script
|
||||
|
@@ -245,6 +245,10 @@ class Configuration:
|
||||
self._args_to_config(config, argname='timeframe_detail',
|
||||
logstring='Parameter --timeframe-detail detected, '
|
||||
'using {} for intra-candle backtesting ...')
|
||||
|
||||
self._args_to_config(config, argname='backtest_show_pair_list',
|
||||
logstring='Parameter --show-pair-list detected.')
|
||||
|
||||
self._args_to_config(config, argname='stake_amount',
|
||||
logstring='Parameter --stake-amount detected, '
|
||||
'overriding stake_amount to: {} ...')
|
||||
|
@@ -32,6 +32,7 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
||||
:param prefix: Prefix to consider (usually FREQTRADE__)
|
||||
:return: Nested dict based on available and relevant variables.
|
||||
"""
|
||||
no_convert = ['CHAT_ID']
|
||||
relevant_vars: Dict[str, Any] = {}
|
||||
|
||||
for env_var, val in sorted(env_dict.items()):
|
||||
@@ -39,9 +40,9 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
||||
logger.info(f"Loading variable '{env_var}'")
|
||||
key = env_var.replace(prefix, '')
|
||||
for k in reversed(key.split('__')):
|
||||
val = {k.lower(): get_var_typed(val) if type(val) != dict else val}
|
||||
val = {k.lower(): get_var_typed(val)
|
||||
if type(val) != dict and k not in no_convert else val}
|
||||
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
||||
|
||||
return relevant_vars
|
||||
|
||||
|
||||
|
@@ -25,6 +25,7 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
||||
'CalmarHyperOptLoss',
|
||||
'MaxDrawDownHyperOptLoss']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||
@@ -55,7 +56,6 @@ ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||
|
||||
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
||||
|
||||
|
||||
# Define decimals per coin for outputs
|
||||
# Only used for outputs.
|
||||
DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's
|
||||
@@ -69,7 +69,6 @@ DUST_PER_COIN = {
|
||||
'ETH': 0.01
|
||||
}
|
||||
|
||||
|
||||
# Source files with destination directories within user-directory
|
||||
USER_DATA_FILES = {
|
||||
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||
@@ -355,13 +354,13 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'dataformat_ohlcv': {
|
||||
'type': 'string',
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'json'
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'json'
|
||||
},
|
||||
'dataformat_trades': {
|
||||
'type': 'string',
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'jsongz'
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'jsongz'
|
||||
}
|
||||
},
|
||||
'definitions': {
|
||||
|
@@ -16,6 +16,7 @@ class SignalTagType(Enum):
|
||||
Enum for signal columns
|
||||
"""
|
||||
ENTER_TAG = "enter_tag"
|
||||
EXIT_TAG = "exit_tag"
|
||||
|
||||
|
||||
class SignalDirection(Enum):
|
||||
|
@@ -19,3 +19,4 @@ from freqtrade.exchange.gateio import Gateio
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.kucoin import Kucoin
|
||||
from freqtrade.exchange.okex import Okex
|
||||
|
@@ -1887,7 +1887,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
||||
|
||||
|
||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||
return exchange_name in ['bittrex', 'binance', 'kraken']
|
||||
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okex']
|
||||
|
||||
|
||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||
|
18
freqtrade/exchange/okex.py
Normal file
18
freqtrade/exchange/okex.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Okex(Exchange):
|
||||
"""
|
||||
Okex exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 100,
|
||||
}
|
@@ -230,11 +230,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
if len(open_trades) != 0:
|
||||
msg = {
|
||||
'type': RPCMessageType.WARNING,
|
||||
'status': f"{len(open_trades)} open trades active.\n\n"
|
||||
f"Handle these trades manually on {self.exchange.name}, "
|
||||
f"or '/start' the bot again and use '/stopbuy' "
|
||||
f"to handle open trades gracefully. \n"
|
||||
f"{'Trades are simulated.' if self.config['dry_run'] else ''}",
|
||||
'status': f"{len(open_trades)} open trades active.\n\n"
|
||||
f"Handle these trades manually on {self.exchange.name}, "
|
||||
f"or '/start' the bot again and use '/stopbuy' "
|
||||
f"to handle open trades gracefully. \n"
|
||||
f"{'Trades are simulated.' if self.config['dry_run'] else ''}",
|
||||
}
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
@@ -863,6 +863,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.debug('Handling %s ...', trade)
|
||||
|
||||
(enter, exit_) = (False, False)
|
||||
exit_tag = None
|
||||
exit_signal_type = "exit_short" if trade.is_short else "exit_long"
|
||||
|
||||
# TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal
|
||||
@@ -871,7 +872,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||
self.strategy.timeframe)
|
||||
|
||||
(enter, exit_) = self.strategy.get_exit_signal(
|
||||
(enter, exit_, exit_tag) = self.strategy.get_exit_signal(
|
||||
trade.pair,
|
||||
self.strategy.timeframe,
|
||||
analyzed_df,
|
||||
@@ -880,7 +881,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
logger.debug('checking exit')
|
||||
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side)
|
||||
if self._check_and_execute_exit(trade, exit_rate, enter, exit_):
|
||||
if self._check_and_execute_exit(trade, exit_rate, enter, exit_, exit_tag):
|
||||
return True
|
||||
|
||||
logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
|
||||
@@ -1029,7 +1030,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
||||
enter: bool, exit_: bool) -> bool:
|
||||
enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
|
||||
"""
|
||||
Check and execute trade exit
|
||||
"""
|
||||
@@ -1043,8 +1044,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
|
||||
if should_exit.sell_flag:
|
||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}')
|
||||
self.execute_trade_exit(trade, exit_rate, should_exit)
|
||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}'
|
||||
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
||||
self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -1258,6 +1260,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade: Trade,
|
||||
limit: float,
|
||||
sell_reason: SellCheckTuple, # TODO-lev update to exit_reason
|
||||
exit_tag: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Executes a trade exit for the given trade and limit
|
||||
@@ -1337,7 +1340,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.open_order_id = order['id']
|
||||
trade.sell_order_status = ''
|
||||
trade.close_rate_requested = limit
|
||||
trade.sell_reason = sell_reason.sell_reason
|
||||
trade.sell_reason = exit_tag or sell_reason.sell_reason
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
@@ -1378,6 +1381,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
'profit_ratio': profit_ratio,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.utcnow(),
|
||||
@@ -1421,6 +1425,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
'profit_ratio': profit_ratio,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||
@@ -1467,6 +1472,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
trade.update_order(order)
|
||||
|
||||
if self.exchange.check_order_canceled_empty(order):
|
||||
# Trade has been cancelled on exchange
|
||||
# Handling of this will happen in check_handle_timeout.
|
||||
return True
|
||||
|
||||
# Try update amount (binance-fix)
|
||||
try:
|
||||
new_amount = self.get_real_amount(trade, order)
|
||||
@@ -1478,10 +1488,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
except DependencyException as exception:
|
||||
logger.warning("Could not update trade amount: %s", exception)
|
||||
|
||||
if self.exchange.check_order_canceled_empty(order):
|
||||
# Trade has been cancelled on exchange
|
||||
# Handling of this will happen in check_handle_timeout.
|
||||
return True
|
||||
trade.update(order)
|
||||
Trade.commit()
|
||||
|
||||
|
@@ -46,6 +46,7 @@ ELONG_IDX = 6 # Exit long
|
||||
SHORT_IDX = 7
|
||||
ESHORT_IDX = 8 # Exit short
|
||||
ENTER_TAG_IDX = 9
|
||||
EXIT_TAG_IDX = 10
|
||||
|
||||
|
||||
class Backtesting:
|
||||
@@ -257,7 +258,7 @@ class Backtesting:
|
||||
# Every change to this headers list must evaluate further usages of the resulting tuple
|
||||
# and eventually change the constants for indexes at the top
|
||||
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||
'enter_short', 'exit_short', 'enter_tag']
|
||||
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
|
||||
data: Dict = {}
|
||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
||||
|
||||
@@ -283,7 +284,7 @@ class Backtesting:
|
||||
if col in df_analyzed.columns:
|
||||
df_analyzed.loc[:, col] = df_analyzed.loc[:, col].shift(1)
|
||||
else:
|
||||
df_analyzed.loc[:, col] = 0 if col != 'enter_tag' else None
|
||||
df_analyzed.loc[:, col] = 0 if col not in ('enter_tag', 'exit_tag') else None
|
||||
|
||||
# Update dataprovider cache
|
||||
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
|
||||
@@ -326,7 +327,9 @@ class Backtesting:
|
||||
# Worst case: price ticks tiny bit above open and dives down.
|
||||
stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct))
|
||||
assert stop_rate < sell_row[HIGH_IDX]
|
||||
return stop_rate
|
||||
# Limit lower-end to candle low to avoid sells below the low.
|
||||
# This still remains "worst case" - but "worst realistic case".
|
||||
return max(sell_row[LOW_IDX], stop_rate)
|
||||
|
||||
# Set close_rate to stoploss
|
||||
return trade.stop_loss
|
||||
@@ -375,6 +378,16 @@ class Backtesting:
|
||||
if sell.sell_flag:
|
||||
trade.close_date = sell_candle_time
|
||||
trade.sell_reason = sell.sell_reason
|
||||
|
||||
# Checks and adds an exit tag, after checking that the length of the
|
||||
# sell_row has the length for an exit tag column
|
||||
if(
|
||||
len(sell_row) > EXIT_TAG_IDX
|
||||
and sell_row[EXIT_TAG_IDX] is not None
|
||||
and len(sell_row[EXIT_TAG_IDX]) > 0
|
||||
):
|
||||
trade.sell_reason = sell_row[EXIT_TAG_IDX]
|
||||
|
||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
|
||||
|
64
freqtrade/optimize/hyperopt_loss_calmar.py
Normal file
64
freqtrade/optimize/hyperopt_loss_calmar.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
CalmarHyperOptLoss
|
||||
|
||||
This module defines the alternative HyperOptLoss class which can be used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from math import sqrt as msqrt
|
||||
from typing import Any, Dict
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.btanalysis import calculate_max_drawdown
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
class CalmarHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Defines the loss function for hyperopt.
|
||||
|
||||
This implementation uses the Calmar Ratio calculation.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Dict,
|
||||
processed: Dict[str, DataFrame],
|
||||
backtest_stats: Dict[str, Any],
|
||||
*args,
|
||||
**kwargs
|
||||
) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for more optimal results.
|
||||
|
||||
Uses Calmar Ratio calculation.
|
||||
"""
|
||||
total_profit = backtest_stats["profit_total"]
|
||||
days_period = (max_date - min_date).days
|
||||
|
||||
# adding slippage of 0.1% per trade
|
||||
total_profit = total_profit - 0.0005
|
||||
expected_returns_mean = total_profit.sum() / days_period * 100
|
||||
|
||||
# calculate max drawdown
|
||||
try:
|
||||
_, _, _, high_val, low_val = calculate_max_drawdown(
|
||||
results, value_col="profit_abs"
|
||||
)
|
||||
max_drawdown = (high_val - low_val) / high_val
|
||||
except ValueError:
|
||||
max_drawdown = 0
|
||||
|
||||
if max_drawdown != 0:
|
||||
calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365)
|
||||
else:
|
||||
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
|
||||
calmar_ratio = -20.0
|
||||
|
||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||
return -calmar_ratio
|
@@ -1,4 +1,3 @@
|
||||
|
||||
import io
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
@@ -64,10 +63,11 @@ class HyperoptTools():
|
||||
'export_time': datetime.now(timezone.utc),
|
||||
}
|
||||
logger.info(f"Dumping parameters to {filename}")
|
||||
rapidjson.dump(final_params, filename.open('w'), indent=2,
|
||||
default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||
)
|
||||
with filename.open('w') as f:
|
||||
rapidjson.dump(final_params, f, indent=2,
|
||||
default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict):
|
||||
|
@@ -55,6 +55,15 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
|
||||
'Win Draw Loss Win%']
|
||||
|
||||
|
||||
def _get_line_header_sell(first_column: str, stake_currency: str) -> List[str]:
|
||||
"""
|
||||
Generate header lines (goes in line with _generate_result_line())
|
||||
"""
|
||||
return [first_column, 'Sells', 'Avg Profit %', 'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||
'Win Draw Loss Win%']
|
||||
|
||||
|
||||
def _generate_wins_draws_losses(wins, draws, losses):
|
||||
if wins > 0 and losses == 0:
|
||||
wl_ratio = '100'
|
||||
@@ -127,6 +136,71 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b
|
||||
return tabular_data
|
||||
|
||||
|
||||
def generate_tag_metrics(tag_type: str,
|
||||
starting_balance: int,
|
||||
results: DataFrame,
|
||||
skip_nan: bool = False) -> List[Dict]:
|
||||
"""
|
||||
Generates and returns a list of metrics for the given tag trades and the results dataframe
|
||||
:param starting_balance: Starting balance
|
||||
:param results: Dataframe containing the backtest results
|
||||
:param skip_nan: Print "left open" open trades
|
||||
:return: List of Dicts containing the metrics per pair
|
||||
"""
|
||||
|
||||
tabular_data = []
|
||||
|
||||
if tag_type in results.columns:
|
||||
for tag, count in results[tag_type].value_counts().iteritems():
|
||||
result = results[results[tag_type] == tag]
|
||||
if skip_nan and result['profit_abs'].isnull().all():
|
||||
continue
|
||||
|
||||
tabular_data.append(_generate_tag_result_line(result, starting_balance, tag))
|
||||
|
||||
# Sort by total profit %:
|
||||
tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
|
||||
|
||||
# Append Total
|
||||
tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
|
||||
return tabular_data
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
|
||||
"""
|
||||
Generate one result dict, with "first_column" as key.
|
||||
"""
|
||||
profit_sum = result['profit_ratio'].sum()
|
||||
# (end-capital - starting capital) / starting capital
|
||||
profit_total = result['profit_abs'].sum() / starting_balance
|
||||
|
||||
return {
|
||||
'key': first_column,
|
||||
'trades': len(result),
|
||||
'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0,
|
||||
'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0,
|
||||
'profit_sum': profit_sum,
|
||||
'profit_sum_pct': round(profit_sum * 100.0, 2),
|
||||
'profit_total_abs': result['profit_abs'].sum(),
|
||||
'profit_total': profit_total,
|
||||
'profit_total_pct': round(profit_total * 100.0, 2),
|
||||
'duration_avg': str(timedelta(
|
||||
minutes=round(result['trade_duration'].mean()))
|
||||
) if not result.empty else '0:00',
|
||||
# 'duration_max': str(timedelta(
|
||||
# minutes=round(result['trade_duration'].max()))
|
||||
# ) if not result.empty else '0:00',
|
||||
# 'duration_min': str(timedelta(
|
||||
# minutes=round(result['trade_duration'].min()))
|
||||
# ) if not result.empty else '0:00',
|
||||
'wins': len(result[result['profit_abs'] > 0]),
|
||||
'draws': len(result[result['profit_abs'] == 0]),
|
||||
'losses': len(result[result['profit_abs'] < 0]),
|
||||
}
|
||||
|
||||
|
||||
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
@@ -347,6 +421,10 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||
starting_balance=starting_balance,
|
||||
results=results, skip_nan=False)
|
||||
|
||||
buy_tag_results = generate_tag_metrics("buy_tag", starting_balance=starting_balance,
|
||||
results=results, skip_nan=False)
|
||||
|
||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
||||
results=results)
|
||||
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||
@@ -370,6 +448,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
'best_pair': best_pair,
|
||||
'worst_pair': worst_pair,
|
||||
'results_per_pair': pair_results,
|
||||
'results_per_buy_tag': buy_tag_results,
|
||||
'sell_reason_summary': sell_reason_stats,
|
||||
'left_open_trades': left_open_results,
|
||||
# 'days_breakdown_stats': days_breakdown_stats,
|
||||
@@ -542,6 +621,37 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
|
||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||
"""
|
||||
Generates and returns a text table for the given backtest data and the results dataframe
|
||||
:param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
|
||||
:param stake_currency: stake-currency - used to correctly name headers
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
if(tag_type == "buy_tag"):
|
||||
headers = _get_line_header("TAG", stake_currency)
|
||||
else:
|
||||
headers = _get_line_header_sell("TAG", stake_currency)
|
||||
floatfmt = _get_line_floatfmt(stake_currency)
|
||||
output = [
|
||||
[
|
||||
t['key'] if t['key'] is not None and len(
|
||||
t['key']) > 0 else "OTHER",
|
||||
t['trades'],
|
||||
t['profit_mean_pct'],
|
||||
t['profit_sum_pct'],
|
||||
t['profit_total_abs'],
|
||||
t['profit_total_pct'],
|
||||
t['duration_avg'],
|
||||
_generate_wins_draws_losses(
|
||||
t['wins'],
|
||||
t['draws'],
|
||||
t['losses'])] for t in tag_results]
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(output, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
|
||||
stake_currency: str, period: str) -> str:
|
||||
"""
|
||||
@@ -687,6 +797,16 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
if results.get('results_per_buy_tag') is not None:
|
||||
table = text_table_tags(
|
||||
"buy_tag",
|
||||
results['results_per_buy_tag'],
|
||||
stake_currency=stake_currency)
|
||||
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
|
||||
stake_currency=stake_currency)
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
@@ -714,6 +834,7 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print('=' * len(table.splitlines()[0]))
|
||||
|
||||
print()
|
||||
|
||||
|
||||
@@ -735,3 +856,13 @@ def show_backtest_results(config: Dict, backtest_stats: Dict):
|
||||
print(table)
|
||||
print('=' * len(table.splitlines()[0]))
|
||||
print('\nFor more details, please look at the detail tables above')
|
||||
|
||||
|
||||
def show_sorted_pairlist(config: Dict, backtest_stats: Dict):
|
||||
if config.get('backtest_show_pair_list', False):
|
||||
for strategy, results in backtest_stats['strategy'].items():
|
||||
print(f"Pairs for Strategy {strategy}: \n[")
|
||||
for result in results['results_per_pair']:
|
||||
if result["key"] != 'TOTAL':
|
||||
print(f'"{result["key"]}", // {round(result["profit_mean_pct"], 2)}%')
|
||||
print("]")
|
||||
|
@@ -7,11 +7,15 @@ class SKDecimal(Integer):
|
||||
def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None,
|
||||
name=None, dtype=np.int64):
|
||||
self.decimals = decimals
|
||||
_low = int(low * pow(10, self.decimals))
|
||||
_high = int(high * pow(10, self.decimals))
|
||||
|
||||
self.pow_dot_one = pow(0.1, self.decimals)
|
||||
self.pow_ten = pow(10, self.decimals)
|
||||
|
||||
_low = int(low * self.pow_ten)
|
||||
_high = int(high * self.pow_ten)
|
||||
# trunc to precision to avoid points out of space
|
||||
self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals)
|
||||
self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals)
|
||||
self.low_orig = round(_low * self.pow_dot_one, self.decimals)
|
||||
self.high_orig = round(_high * self.pow_dot_one, self.decimals)
|
||||
|
||||
super().__init__(_low, _high, prior, base, transform, name, dtype)
|
||||
|
||||
@@ -25,9 +29,9 @@ class SKDecimal(Integer):
|
||||
return self.low_orig <= point <= self.high_orig
|
||||
|
||||
def transform(self, Xt):
|
||||
aa = [int(x * pow(10, self.decimals)) for x in Xt]
|
||||
return super().transform(aa)
|
||||
return super().transform([int(v * self.pow_ten) for v in Xt])
|
||||
|
||||
def inverse_transform(self, Xt):
|
||||
res = super().inverse_transform(Xt)
|
||||
return [round(x * pow(0.1, self.decimals), self.decimals) for x in res]
|
||||
# equivalent to [round(x * pow(0.1, self.decimals), self.decimals) for x in res]
|
||||
return [int(v) / self.pow_ten for v in res]
|
||||
|
@@ -1080,13 +1080,131 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
return [
|
||||
{
|
||||
'pair': pair,
|
||||
'profit': profit,
|
||||
'profit_ratio': profit,
|
||||
'profit': round(profit * 100, 2), # Compatibility mode
|
||||
'profit_pct': round(profit * 100, 2),
|
||||
'profit_abs': profit_abs,
|
||||
'count': count
|
||||
}
|
||||
for pair, profit, profit_abs, count in pair_rates
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_buy_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, based on buy tag performance
|
||||
Can either be average for all pairs or a specific pair provided
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if(pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
buy_tag_perf = Trade.query.with_entities(
|
||||
Trade.buy_tag,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.buy_tag) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
||||
return [
|
||||
{
|
||||
'buy_tag': buy_tag if buy_tag is not None else "Other",
|
||||
'profit_ratio': profit,
|
||||
'profit_pct': round(profit * 100, 2),
|
||||
'profit_abs': profit_abs,
|
||||
'count': count
|
||||
}
|
||||
for buy_tag, profit, profit_abs, count in buy_tag_perf
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_sell_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, based on sell reason performance
|
||||
Can either be average for all pairs or a specific pair provided
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if(pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
sell_tag_perf = Trade.query.with_entities(
|
||||
Trade.sell_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.sell_reason) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
||||
return [
|
||||
{
|
||||
'sell_reason': sell_reason if sell_reason is not None else "Other",
|
||||
'profit_ratio': profit,
|
||||
'profit_pct': round(profit * 100, 2),
|
||||
'profit_abs': profit_abs,
|
||||
'count': count
|
||||
}
|
||||
for sell_reason, profit, profit_abs, count in sell_tag_perf
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, based on buy_tag + sell_reason performance
|
||||
Can either be average for all pairs or a specific pair provided
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if(pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
mix_tag_perf = Trade.query.with_entities(
|
||||
Trade.id,
|
||||
Trade.buy_tag,
|
||||
Trade.sell_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.id) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
||||
return_list: List[Dict] = []
|
||||
for id, buy_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
|
||||
buy_tag = buy_tag if buy_tag is not None else "Other"
|
||||
sell_reason = sell_reason if sell_reason is not None else "Other"
|
||||
|
||||
if(sell_reason is not None and buy_tag is not None):
|
||||
mix_tag = buy_tag + " " + sell_reason
|
||||
i = 0
|
||||
if not any(item["mix_tag"] == mix_tag for item in return_list):
|
||||
return_list.append({'mix_tag': mix_tag,
|
||||
'profit': profit,
|
||||
'profit_abs': profit_abs,
|
||||
'count': count})
|
||||
else:
|
||||
while i < len(return_list):
|
||||
if return_list[i]["mix_tag"] == mix_tag:
|
||||
return_list[i] = {
|
||||
'mix_tag': mix_tag,
|
||||
'profit': profit + return_list[i]["profit"],
|
||||
'profit_abs': profit_abs + return_list[i]["profit_abs"],
|
||||
'count': 1 + return_list[i]["count"]}
|
||||
i += 1
|
||||
|
||||
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in return_list]
|
||||
return return_list
|
||||
|
||||
@staticmethod
|
||||
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
|
||||
"""
|
||||
@@ -1123,7 +1241,7 @@ class PairLock(_DECL_BASE):
|
||||
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
|
||||
lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
|
||||
return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, '
|
||||
f'lock_end_time={lock_end_time})')
|
||||
f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})')
|
||||
|
||||
@staticmethod
|
||||
def query_pair_locks(pair: Optional[str], now: datetime) -> Query:
|
||||
@@ -1132,7 +1250,6 @@ class PairLock(_DECL_BASE):
|
||||
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||
"""
|
||||
|
||||
filters = [PairLock.lock_end_time > now,
|
||||
# Only active locks
|
||||
PairLock.active.is_(True), ]
|
||||
|
@@ -103,6 +103,36 @@ class PairLocks():
|
||||
if PairLocks.use_db:
|
||||
PairLock.query.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def unlock_reason(reason: str, now: Optional[datetime] = None) -> None:
|
||||
"""
|
||||
Release all locks for this reason.
|
||||
:param reason: Which reason to unlock
|
||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||
defaults to datetime.now(timezone.utc)
|
||||
"""
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
if PairLocks.use_db:
|
||||
# used in live modes
|
||||
logger.info(f"Releasing all locks with reason '{reason}':")
|
||||
filters = [PairLock.lock_end_time > now,
|
||||
PairLock.active.is_(True),
|
||||
PairLock.reason == reason
|
||||
]
|
||||
locks = PairLock.query.filter(*filters)
|
||||
for lock in locks:
|
||||
logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.")
|
||||
lock.active = False
|
||||
PairLock.query.session.commit()
|
||||
else:
|
||||
# used in backtesting mode; don't show log messages for speed
|
||||
locks = PairLocks.get_pair_locks(None)
|
||||
for lock in locks:
|
||||
if lock.reason == reason:
|
||||
lock.active = False
|
||||
|
||||
@staticmethod
|
||||
def is_global_lock(now: Optional[datetime] = None) -> bool:
|
||||
"""
|
||||
@@ -128,7 +158,9 @@ class PairLocks():
|
||||
|
||||
@staticmethod
|
||||
def get_all_locks() -> List[PairLock]:
|
||||
|
||||
"""
|
||||
Return all locks, also locks with expired end date
|
||||
"""
|
||||
if PairLocks.use_db:
|
||||
return PairLock.query.all()
|
||||
else:
|
||||
|
@@ -91,7 +91,7 @@ class IResolver:
|
||||
logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'")
|
||||
for entry in directory.iterdir():
|
||||
# Only consider python files
|
||||
if not str(entry).endswith('.py'):
|
||||
if entry.suffix != '.py':
|
||||
logger.debug('Ignoring %s', entry)
|
||||
continue
|
||||
if entry.is_symlink() and not entry.is_file():
|
||||
@@ -169,7 +169,7 @@ class IResolver:
|
||||
objects = []
|
||||
for entry in directory.iterdir():
|
||||
# Only consider python files
|
||||
if not str(entry).endswith('.py'):
|
||||
if entry.suffix != '.py':
|
||||
logger.debug('Ignoring %s', entry)
|
||||
continue
|
||||
module_path = entry.resolve()
|
||||
|
@@ -56,17 +56,21 @@ class StrategyResolver(IResolver):
|
||||
if strategy._ft_params_from_file:
|
||||
# Set parameters from Hyperopt results file
|
||||
params = strategy._ft_params_from_file
|
||||
strategy.minimal_roi = params.get('roi', strategy.minimal_roi)
|
||||
strategy.minimal_roi = params.get('roi', getattr(strategy, 'minimal_roi', {}))
|
||||
|
||||
strategy.stoploss = params.get('stoploss', {}).get('stoploss', strategy.stoploss)
|
||||
strategy.stoploss = params.get('stoploss', {}).get(
|
||||
'stoploss', getattr(strategy, 'stoploss', -0.1))
|
||||
trailing = params.get('trailing', {})
|
||||
strategy.trailing_stop = trailing.get('trailing_stop', strategy.trailing_stop)
|
||||
strategy.trailing_stop_positive = trailing.get('trailing_stop_positive',
|
||||
strategy.trailing_stop_positive)
|
||||
strategy.trailing_stop = trailing.get(
|
||||
'trailing_stop', getattr(strategy, 'trailing_stop', False))
|
||||
strategy.trailing_stop_positive = trailing.get(
|
||||
'trailing_stop_positive', getattr(strategy, 'trailing_stop_positive', None))
|
||||
strategy.trailing_stop_positive_offset = trailing.get(
|
||||
'trailing_stop_positive_offset', strategy.trailing_stop_positive_offset)
|
||||
'trailing_stop_positive_offset',
|
||||
getattr(strategy, 'trailing_stop_positive_offset', 0))
|
||||
strategy.trailing_only_offset_is_reached = trailing.get(
|
||||
'trailing_only_offset_is_reached', strategy.trailing_only_offset_is_reached)
|
||||
'trailing_only_offset_is_reached',
|
||||
getattr(strategy, 'trailing_only_offset_is_reached', 0.0))
|
||||
|
||||
# Set attributes
|
||||
# Check if we need to override configuration
|
||||
|
@@ -63,6 +63,8 @@ class Count(BaseModel):
|
||||
class PerformanceEntry(BaseModel):
|
||||
pair: str
|
||||
profit: float
|
||||
profit_ratio: float
|
||||
profit_pct: float
|
||||
profit_abs: float
|
||||
count: int
|
||||
|
||||
|
@@ -106,7 +106,7 @@ class RPC:
|
||||
val = {
|
||||
'dry_run': config['dry_run'],
|
||||
'stake_currency': config['stake_currency'],
|
||||
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||
'stake_amount': config['stake_amount'],
|
||||
'available_capital': config.get('available_capital'),
|
||||
'max_open_trades': (config['max_open_trades']
|
||||
@@ -683,10 +683,36 @@ class RPC:
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
pair_rates = Trade.get_overall_performance()
|
||||
# Round and convert to %
|
||||
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates]
|
||||
|
||||
return pair_rates
|
||||
|
||||
def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for buy tag performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
buy_tags = Trade.get_buy_tag_performance(pair)
|
||||
|
||||
return buy_tags
|
||||
|
||||
def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for sell reason performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
sell_reasons = Trade.get_sell_reason_performance(pair)
|
||||
|
||||
return sell_reasons
|
||||
|
||||
def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for mix tag (buy_tag + sell_reason) performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
mix_tags = Trade.get_mix_tag_performance(pair)
|
||||
|
||||
return mix_tags
|
||||
|
||||
def _rpc_count(self) -> Dict[str, float]:
|
||||
""" Returns the number of trades running """
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
|
@@ -107,8 +107,8 @@ class Telegram(RPCHandler):
|
||||
# this needs refactoring of the whole telegram module (same
|
||||
# problem in _help()).
|
||||
valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$',
|
||||
r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$',
|
||||
r'/profit$', r'/profit \d+',
|
||||
r'/trades$', r'/performance$', r'/buys', r'/sells', r'/mix_tags',
|
||||
r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
|
||||
r'/stats$', r'/count$', r'/locks$', r'/balance$',
|
||||
r'/stopbuy$', r'/reload_config$', r'/show_config$',
|
||||
r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$',
|
||||
@@ -154,6 +154,9 @@ class Telegram(RPCHandler):
|
||||
CommandHandler('trades', self._trades),
|
||||
CommandHandler('delete', self._delete_trade),
|
||||
CommandHandler('performance', self._performance),
|
||||
CommandHandler('buys', self._buy_tag_performance),
|
||||
CommandHandler('sells', self._sell_reason_performance),
|
||||
CommandHandler('mix_tags', self._mix_tag_performance),
|
||||
CommandHandler('stats', self._stats),
|
||||
CommandHandler('daily', self._daily),
|
||||
CommandHandler('count', self._count),
|
||||
@@ -175,6 +178,10 @@ class Telegram(RPCHandler):
|
||||
CallbackQueryHandler(self._profit, pattern='update_profit'),
|
||||
CallbackQueryHandler(self._balance, pattern='update_balance'),
|
||||
CallbackQueryHandler(self._performance, pattern='update_performance'),
|
||||
CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'),
|
||||
CallbackQueryHandler(self._sell_reason_performance,
|
||||
pattern='update_sell_reason_performance'),
|
||||
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
||||
CallbackQueryHandler(self._count, pattern='update_count'),
|
||||
CallbackQueryHandler(self._forcebuy_inline),
|
||||
]
|
||||
@@ -238,6 +245,7 @@ class Telegram(RPCHandler):
|
||||
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
||||
|
||||
msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None
|
||||
msg['emoji'] = self._get_sell_emoji(msg)
|
||||
|
||||
# Check if all sell properties are available.
|
||||
@@ -253,6 +261,7 @@ class Telegram(RPCHandler):
|
||||
|
||||
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
|
||||
"*Profit:* `{profit_percent:.2f}%{profit_extra}`\n"
|
||||
"*Buy Tag:* `{buy_tag}`\n"
|
||||
"*Sell Reason:* `{sell_reason}`\n"
|
||||
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
|
||||
"*Amount:* `{amount:.8f}`\n"
|
||||
@@ -852,7 +861,7 @@ class Telegram(RPCHandler):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['pair']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit']:.2f}%) "
|
||||
f"({trade['profit_pct']:.2f}%) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
@@ -867,6 +876,111 @@ class Telegram(RPCHandler):
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _buy_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /buys PAIR .
|
||||
Shows a performance statistic from finished trades
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
pair = None
|
||||
if context.args and isinstance(context.args[0], str):
|
||||
pair = context.args[0]
|
||||
|
||||
trades = self._rpc._rpc_buy_tag_performance(pair)
|
||||
output = "<b>Buy Tag Performance:</b>\n"
|
||||
for i, trade in enumerate(trades):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['buy_tag']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit_pct']:.2f}%) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML)
|
||||
output = stat_line
|
||||
else:
|
||||
output += stat_line
|
||||
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||
reload_able=True, callback_path="update_buy_tag_performance",
|
||||
query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _sell_reason_performance(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /sells.
|
||||
Shows a performance statistic from finished trades
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
pair = None
|
||||
if context.args and isinstance(context.args[0], str):
|
||||
pair = context.args[0]
|
||||
|
||||
trades = self._rpc._rpc_sell_reason_performance(pair)
|
||||
output = "<b>Sell Reason Performance:</b>\n"
|
||||
for i, trade in enumerate(trades):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['sell_reason']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit_pct']:.2f}%) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML)
|
||||
output = stat_line
|
||||
else:
|
||||
output += stat_line
|
||||
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||
reload_able=True, callback_path="update_sell_reason_performance",
|
||||
query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /mix_tags.
|
||||
Shows a performance statistic from finished trades
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
pair = None
|
||||
if context.args and isinstance(context.args[0], str):
|
||||
pair = context.args[0]
|
||||
|
||||
trades = self._rpc._rpc_mix_tag_performance(pair)
|
||||
output = "<b>Mix Tag Performance:</b>\n"
|
||||
for i, trade in enumerate(trades):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['mix_tag']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit']:.2f}%) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML)
|
||||
output = stat_line
|
||||
else:
|
||||
output += stat_line
|
||||
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||
reload_able=True, callback_path="update_mix_tag_performance",
|
||||
query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _count(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
@@ -1033,7 +1147,8 @@ class Telegram(RPCHandler):
|
||||
:return: None
|
||||
"""
|
||||
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
|
||||
"Optionally takes a rate at which to buy.` \n")
|
||||
"Optionally takes a rate at which to buy "
|
||||
"(only applies to limit orders).` \n")
|
||||
message = ("*/start:* `Starts the trader`\n"
|
||||
"*/stop:* `Stops the trader`\n"
|
||||
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
|
||||
@@ -1042,6 +1157,9 @@ class Telegram(RPCHandler):
|
||||
" *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"
|
||||
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
|
||||
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
|
||||
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
|
||||
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
|
||||
"over the last n days`\n"
|
||||
|
@@ -381,7 +381,8 @@ class HyperStrategyMixin(object):
|
||||
if filename.is_file():
|
||||
logger.info(f"Loading parameters from file {filename}")
|
||||
try:
|
||||
params = json_load(filename.open('r'))
|
||||
with filename.open('r') as f:
|
||||
params = json_load(f)
|
||||
if params.get('strategy_name') != self.__class__.__name__:
|
||||
raise OperationalException('Invalid parameter file provided.')
|
||||
return params
|
||||
|
@@ -65,9 +65,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
_populate_fun_len: int = 0
|
||||
_buy_fun_len: int = 0
|
||||
_sell_fun_len: int = 0
|
||||
_ft_params_from_file: Dict = {}
|
||||
_ft_params_from_file: Dict
|
||||
# associated minimal roi
|
||||
minimal_roi: Dict
|
||||
minimal_roi: Dict = {}
|
||||
|
||||
# associated stoploss
|
||||
stoploss: float
|
||||
@@ -462,6 +462,15 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"""
|
||||
PairLocks.unlock_pair(pair, datetime.now(timezone.utc))
|
||||
|
||||
def unlock_reason(self, reason: str) -> None:
|
||||
"""
|
||||
Unlocks all pairs previously locked using lock_pair with specified reason.
|
||||
Not used by freqtrade itself, but intended to be used if users lock pairs
|
||||
manually from within the strategy, to allow an easy way to unlock pairs.
|
||||
:param reason: Unlock pairs to allow trading again
|
||||
"""
|
||||
PairLocks.unlock_reason(reason, datetime.now(timezone.utc))
|
||||
|
||||
def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool:
|
||||
"""
|
||||
Checks if a pair is currently locked
|
||||
@@ -521,6 +530,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
dataframe[SignalType.ENTER_SHORT.value] = 0
|
||||
dataframe[SignalType.EXIT_SHORT.value] = 0
|
||||
dataframe[SignalTagType.ENTER_TAG.value] = None
|
||||
dataframe[SignalTagType.EXIT_TAG.value] = None
|
||||
|
||||
# Other Defs in strategy that want to be called every loop here
|
||||
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
|
||||
@@ -634,7 +644,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
timeframe: str,
|
||||
dataframe: DataFrame,
|
||||
is_short: bool = None
|
||||
) -> Tuple[bool, bool]:
|
||||
) -> Tuple[bool, bool, Optional[str]]:
|
||||
"""
|
||||
Calculates current exit signal based based on the buy/short or sell/exit_short
|
||||
columns of the dataframe.
|
||||
@@ -648,19 +658,21 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"""
|
||||
latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe)
|
||||
if latest is None:
|
||||
return False, False
|
||||
return False, False, None
|
||||
|
||||
if is_short:
|
||||
enter = latest.get(SignalType.ENTER_SHORT.value, 0) == 1
|
||||
exit_ = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
|
||||
|
||||
else:
|
||||
enter = latest[SignalType.ENTER_LONG.value] == 1
|
||||
exit_ = latest.get(SignalType.EXIT_LONG.value, 0) == 1
|
||||
exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None)
|
||||
|
||||
logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) "
|
||||
f"enter={enter} exit={exit_}")
|
||||
|
||||
return enter, exit_
|
||||
return enter, exit_, exit_tag
|
||||
|
||||
def get_entry_signal(
|
||||
self,
|
||||
|
@@ -10,8 +10,7 @@
|
||||
"stake_currency": "{{ stake_currency }}",
|
||||
"stake_amount": {{ stake_amount }},
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "{{ fiat_display_currency }}",
|
||||
"timeframe": "{{ timeframe }}",
|
||||
"fiat_display_currency": "{{ fiat_display_currency }}",{{ ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }}
|
||||
"dry_run": {{ dry_run | lower }},
|
||||
"cancel_open_orders_on_exit": false,
|
||||
"unfilledtimeout": {
|
||||
|
12
freqtrade/templates/subtemplates/exchange_okex.j2
Normal file
12
freqtrade/templates/subtemplates/exchange_okex.j2
Normal file
@@ -0,0 +1,12 @@
|
||||
"exchange": {
|
||||
"name": "{{ exchange_name | lower }}",
|
||||
"key": "{{ exchange_key }}",
|
||||
"secret": "{{ exchange_secret }}",
|
||||
"password": "{{ exchange_key_password }}",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
],
|
||||
"pair_blacklist": [
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user