Merge e6d3a440cd into 0980e7e82d
This commit is contained in:
@@ -14,7 +14,6 @@ from freqtrade.exchange import get_ticker_history
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy.resolver import StrategyResolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -31,6 +30,7 @@ class Analyze(object):
|
||||
Analyze class contains everything the bot need to determine if the situation is good for
|
||||
buying or selling.
|
||||
"""
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
"""
|
||||
Init Analyze
|
||||
@@ -195,10 +195,41 @@ class Analyze(object):
|
||||
:return True if bot should sell at current rate
|
||||
"""
|
||||
current_profit = trade.calc_profit_percent(current_rate)
|
||||
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
|
||||
|
||||
if trade.stop_loss is None:
|
||||
# initially adjust the stop loss to the base value
|
||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss)
|
||||
|
||||
# evaluate if the stoploss was hit
|
||||
if self.strategy.stoploss is not None and trade.stop_loss >= current_rate:
|
||||
|
||||
if 'trailing_stop' in self.config and self.config['trailing_stop']:
|
||||
logger.warning(
|
||||
"HIT STOP: current price at {:.6f}, stop loss is {:.6f}, "
|
||||
"initial stop loss was at {:.6f}, trade opened at {:.6f}".format(
|
||||
current_rate, trade.stop_loss, trade.initial_stop_loss, trade.open_rate))
|
||||
logger.debug("trailing stop saved us: {:.6f}"
|
||||
.format(trade.stop_loss - trade.initial_stop_loss))
|
||||
|
||||
logger.debug('Stop loss hit.')
|
||||
return True
|
||||
|
||||
# update the stop loss afterwards, after all by definition it's supposed to be hanging
|
||||
if 'trailing_stop' in self.config and self.config['trailing_stop']:
|
||||
|
||||
# check if we have a special stop loss for positive condition
|
||||
# and if profit is positive
|
||||
stop_loss_value = self.strategy.stoploss
|
||||
if isinstance(self.config['trailing_stop'], dict) and \
|
||||
'positive' in self.config['trailing_stop'] and \
|
||||
current_profit > 0:
|
||||
|
||||
logger.debug("using positive stop loss mode: {} since we have profit {}".format(
|
||||
self.config['trailing_stop']['positive'], current_profit))
|
||||
stop_loss_value = self.config['trailing_stop']['positive']
|
||||
|
||||
trade.adjust_stop_loss(current_rate, stop_loss_value)
|
||||
|
||||
# Check if time matches and current rate is above threshold
|
||||
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
||||
for duration, threshold in self.strategy.minimal_roi.items():
|
||||
|
||||
@@ -148,6 +148,12 @@ class Trade(_DECL_BASE):
|
||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
close_date = Column(DateTime)
|
||||
open_order_id = Column(String)
|
||||
# absolute value of the stop loss
|
||||
stop_loss = Column(Float, nullable=True, default=0.0)
|
||||
# absolute value of the initial stop loss
|
||||
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
||||
# absolute value of the highest reached price
|
||||
max_rate = Column(Float, nullable=True, default=0.0)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
|
||||
@@ -158,6 +164,50 @@ class Trade(_DECL_BASE):
|
||||
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
||||
)
|
||||
|
||||
def adjust_stop_loss(self, current_price, stoploss):
|
||||
"""
|
||||
|
||||
this adjusts the stop loss to it's most recently observed
|
||||
setting
|
||||
:param current_price:
|
||||
:param stoploss:
|
||||
:return:
|
||||
"""
|
||||
|
||||
new_loss = Decimal(current_price * (1 - abs(stoploss)))
|
||||
|
||||
# keeping track of the highest observed rate for this trade
|
||||
if self.max_rate is None:
|
||||
self.max_rate = current_price
|
||||
else:
|
||||
if current_price > self.max_rate:
|
||||
self.max_rate = current_price
|
||||
|
||||
# no stop loss assigned yet
|
||||
if self.stop_loss is None or self.stop_loss == 0:
|
||||
logger.debug("assigning new stop loss")
|
||||
self.stop_loss = new_loss
|
||||
self.initial_stop_loss = new_loss
|
||||
|
||||
# evaluate if the stop loss needs to be updated
|
||||
else:
|
||||
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
||||
self.stop_loss = new_loss
|
||||
logger.debug("adjusted stop loss")
|
||||
else:
|
||||
logger.debug("keeping current stop loss")
|
||||
|
||||
logger.debug(
|
||||
"{} - current price {:.8f}, bought at {:.8f} and calculated "
|
||||
"stop loss is at: {:.8f} initial stop at {:.8f}. trailing stop loss saved us: {:.8f} "
|
||||
"and max observed rate was {:.8f}".format(
|
||||
self.pair, current_price, self.open_rate,
|
||||
self.initial_stop_loss,
|
||||
self.stop_loss, float(self.stop_loss) - float(self.initial_stop_loss),
|
||||
self.max_rate
|
||||
|
||||
))
|
||||
|
||||
def update(self, order: Dict) -> None:
|
||||
"""
|
||||
Updates this entity with amount and actual open/close rates.
|
||||
|
||||
@@ -98,10 +98,11 @@ class RPC(object):
|
||||
trade.id,
|
||||
trade.pair,
|
||||
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
||||
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
|
||||
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)),
|
||||
'{:.6f}'.format(trade.amount * current_rate)
|
||||
])
|
||||
|
||||
columns = ['ID', 'Pair', 'Since', 'Profit']
|
||||
columns = ['ID', 'Pair', 'Since', 'Profit', 'Value']
|
||||
df_statuses = DataFrame.from_records(trades_list, columns=columns)
|
||||
df_statuses = df_statuses.set_index(columns[0])
|
||||
# The style used throughout is to return a tuple
|
||||
|
||||
@@ -84,6 +84,7 @@ def load_data_test(what):
|
||||
|
||||
def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||
|
||||
backtesting = Backtesting(config)
|
||||
|
||||
data = load_data_test(contour)
|
||||
@@ -97,6 +98,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||
'realistic': True
|
||||
}
|
||||
)
|
||||
|
||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||
assert len(results) == num_results
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
assert 'just now' in result['Since'].all()
|
||||
assert 'ETH/BTC' in result['Pair'].all()
|
||||
assert '-0.59%' in result['Profit'].all()
|
||||
assert 'Value' in result
|
||||
|
||||
|
||||
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||
|
||||
@@ -29,10 +29,18 @@ def test_load_strategy(result):
|
||||
def test_load_strategy_custom_directory(result):
|
||||
resolver = StrategyResolver()
|
||||
extra_dir = os.path.join('some', 'path')
|
||||
with pytest.raises(
|
||||
FileNotFoundError,
|
||||
match=r".*No such file or directory: '{}'".format(extra_dir)):
|
||||
resolver._load_strategy('TestStrategy', extra_dir)
|
||||
|
||||
if os.name == 'nt':
|
||||
with pytest.raises(
|
||||
FileNotFoundError,
|
||||
match="FileNotFoundError: [WinError 3] The system cannot find the "
|
||||
"path specified: '{}'".format(extra_dir)):
|
||||
resolver._load_strategy('TestStrategy', extra_dir)
|
||||
else:
|
||||
with pytest.raises(
|
||||
FileNotFoundError,
|
||||
match=r".*No such file or directory: '{}'".format(extra_dir)):
|
||||
resolver._load_strategy('TestStrategy', extra_dir)
|
||||
|
||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||
|
||||
@@ -444,6 +444,8 @@ def test_migrate_new(default_conf, fee):
|
||||
close_profit FLOAT,
|
||||
stake_amount FLOAT NOT NULL,
|
||||
amount FLOAT,
|
||||
initial_stop_loss FLOAT,
|
||||
max_rate FLOAT,
|
||||
open_date DATETIME NOT NULL,
|
||||
close_date DATETIME,
|
||||
open_order_id VARCHAR,
|
||||
|
||||
Reference in New Issue
Block a user