Merge branch 'develop' into feat/short
This commit is contained in:
@@ -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': {
|
||||
|
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):
|
||||
|
@@ -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]
|
||||
|
@@ -1123,7 +1123,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 +1132,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
|
||||
|
@@ -1033,7 +1033,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"
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user