Merge branch 'develop' into margin-db

This commit is contained in:
Sam Germain
2021-06-20 01:31:47 -06:00
27 changed files with 302 additions and 223 deletions

View File

@@ -275,7 +275,8 @@ CONF_SCHEMA = {
'default': 'off'
},
}
}
},
'reload': {'type': 'boolean'},
},
'required': ['enabled', 'token', 'chat_id'],
},

View File

@@ -68,6 +68,7 @@ class Binance(Exchange):
amount=amount, price=rate, params=params)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
self._log_exchange_response('create_stoploss_order', order)
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(

View File

@@ -104,6 +104,7 @@ class Exchange:
logger.info('Instance is running with dry_run enabled')
logger.info(f"Using CCXT {ccxt.__version__}")
exchange_config = config['exchange']
self.log_responses = exchange_config.get('log_responses', False)
# Deep merge ft_has with default ft_has options
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
@@ -226,6 +227,11 @@ class Exchange:
"""exchange ccxt precisionMode"""
return self._api.precisionMode
def _log_exchange_response(self, endpoint, response) -> None:
""" Log exchange responses """
if self.log_responses:
logger.info(f"API {endpoint}: {response}")
def ohlcv_candle_limit(self, timeframe: str) -> int:
"""
Exchange ohlcv candle limit
@@ -622,8 +628,10 @@ class Exchange:
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
return self._api.create_order(pair, ordertype, side,
amount, rate_for_order, params)
order = self._api.create_order(pair, ordertype, side,
amount, rate_for_order, params)
self._log_exchange_response('create_order', order)
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
@@ -694,7 +702,9 @@ class Exchange:
if self._config['dry_run']:
return self.fetch_dry_run_order(order_id)
try:
return self._api.fetch_order(order_id, pair)
order = self._api.fetch_order(order_id, pair)
self._log_exchange_response('fetch_order', order)
return order
except ccxt.OrderNotFound as e:
raise RetryableOrderError(
f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
@@ -744,7 +754,9 @@ class Exchange:
return {}
try:
return self._api.cancel_order(order_id, pair)
order = self._api.cancel_order(order_id, pair)
self._log_exchange_response('cancel_order', order)
return order
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Could not cancel order. Message: {e}') from e
@@ -1042,6 +1054,7 @@ class Exchange:
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000))
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
self._log_exchange_response('get_trades_for_order', matched_trades)
return matched_trades
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e

View File

@@ -69,6 +69,7 @@ class Ftx(Exchange):
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
amount=amount, params=params)
self._log_exchange_response('create_stoploss_order', order)
logger.info('stoploss order added for %s. '
'stop price: %s.', pair, stop_price)
return order
@@ -99,12 +100,14 @@ class Ftx(Exchange):
orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
order = [order for order in orders if order['id'] == order_id]
self._log_exchange_response('fetch_stoploss_order', order)
if len(order) == 1:
if order[0].get('status') == 'closed':
# Trigger order was triggered ...
real_order_id = order[0].get('info', {}).get('orderId')
order1 = self._api.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order1)
# Fake type to stop - as this was really a stop order.
order1['id_stop'] = order1['id']
order1['id'] = order_id
@@ -131,7 +134,9 @@ class Ftx(Exchange):
if self._config['dry_run']:
return {}
try:
return self._api.cancel_order(order_id, pair, params={'type': 'stop'})
order = self._api.cancel_order(order_id, pair, params={'type': 'stop'})
self._log_exchange_response('cancel_stoploss_order', order)
return order
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Could not cancel order. Message: {e}') from e

View File

@@ -103,6 +103,7 @@ class Kraken(Exchange):
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
amount=amount, price=stop_price, params=params)
self._log_exchange_response('create_stoploss_order', order)
logger.info('stoploss order added for %s. '
'stop price: %s.', pair, stop_price)
return order

View File

@@ -70,7 +70,7 @@ class FreqtradeBot(LoggingMixin):
PairLocks.timeframe = self.config['timeframe']
self.protections = ProtectionManager(self.config)
self.protections = ProtectionManager(self.config, self.strategy.protections)
# RPC runs in separate threads, can start handling external commands just after
# initialization, even before Freqtradebot has a chance to start its throttling,

View File

@@ -137,7 +137,7 @@ class Backtesting:
if hasattr(strategy, 'protections'):
conf = deepcopy(conf)
conf['protections'] = strategy.protections
self.protections = ProtectionManager(conf)
self.protections = ProtectionManager(self.config, strategy.protections)
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
"""
@@ -225,6 +225,22 @@ class Backtesting:
# sell at open price.
return sell_row[OPEN_IDX]
# Special case: trailing triggers within same candle as trade opened. Assume most
# pessimistic price movement, which is moving just enough to arm stoploss and
# immediately going down to stop price.
if (sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0
and self.strategy.trailing_stop_positive):
if self.strategy.trailing_only_offset_is_reached:
# Worst case: price reaches stop_positive_offset and dives down.
stop_rate = (sell_row[OPEN_IDX] *
(1 + abs(self.strategy.trailing_stop_positive_offset) -
abs(self.strategy.trailing_stop_positive)))
else:
# Worst case: price ticks tiny bit above open and dives down.
stop_rate = sell_row[OPEN_IDX] * (1 - abs(self.strategy.trailing_stop_positive))
assert stop_rate < sell_row[HIGH_IDX]
return stop_rate
# Set close_rate to stoploss
return trade.stop_loss
elif sell.sell_type == (SellType.ROI):

19
freqtrade/optimize/hyperopt_tools.py Normal file → Executable file
View File

@@ -91,7 +91,7 @@ class HyperoptTools():
if print_json:
result_dict: Dict = {}
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
HyperoptTools._params_update_for_json(result_dict, params, s)
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
@@ -104,17 +104,24 @@ class HyperoptTools():
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
@staticmethod
def _params_update_for_json(result_dict, params, space: str) -> None:
if space in params:
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
if (space in params) or (space in non_optimized):
space_params = HyperoptTools._space_params(params, space)
space_non_optimized = HyperoptTools._space_params(non_optimized, space)
all_space_params = space_params
# Merge non optimized params if there are any
if len(space_non_optimized) > 0:
all_space_params = {**space_params, **space_non_optimized}
if space in ['buy', 'sell']:
result_dict.setdefault('params', {}).update(space_params)
result_dict.setdefault('params', {}).update(all_space_params)
elif space == 'roi':
# Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys...
result_dict['minimal_roi'] = {str(k): v for k, v in space_params.items()}
result_dict['minimal_roi'] = {str(k): v for k, v in all_space_params.items()}
else: # 'stoploss', 'trailing'
result_dict.update(space_params)
result_dict.update(all_space_params)
@staticmethod
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:

View File

@@ -556,7 +556,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Backtesting to', strat_results['backtest_end']),
('Max open trades', strat_results['max_open_trades']),
('', ''), # Empty line to improve readability
('Total trades', strat_results['total_trades']),
('Total/Daily Avg Trades',
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"),
('Starting balance', round_coin_value(strat_results['starting_balance'],
strat_results['stake_currency'])),
('Final balance', round_coin_value(strat_results['final_balance'],
@@ -564,7 +565,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
strat_results['stake_currency'])),
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"),
('Trades per day', strat_results['trades_per_day']),
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
strat_results['stake_currency'])),
('Total trade volume', round_coin_value(strat_results['total_volume'],

View File

@@ -83,7 +83,8 @@ class PairListManager():
pairlist = self._pairlist_handlers[0].gen_pairlist(tickers)
# Process all Pairlist Handlers in the chain
for pairlist_handler in self._pairlist_handlers:
# except for the first one, which is the generator.
for pairlist_handler in self._pairlist_handlers[1:]:
pairlist = pairlist_handler.filter_pairlist(pairlist, tickers)
# Validation against blacklist happens after the chain of Pairlist Handlers

View File

@@ -15,11 +15,11 @@ logger = logging.getLogger(__name__)
class ProtectionManager():
def __init__(self, config: dict) -> None:
def __init__(self, config: Dict, protections: List) -> None:
self._config = config
self._protection_handlers: List[IProtection] = []
for protection_handler_config in self._config.get('protections', []):
for protection_handler_config in protections:
protection_handler = ProtectionResolver.load_protection(
protection_handler_config['method'],
config=config,

View File

@@ -113,7 +113,9 @@ class StrategyResolver(IResolver):
- Strategy
- default (if not None)
"""
if attribute in config:
if (attribute in config
and not isinstance(getattr(type(strategy), 'my_property', None), property)):
# Ensure Properties are not overwritten
setattr(strategy, attribute, config[attribute])
logger.info("Override strategy '%s' with value in config file: %s.",
attribute, config[attribute])

View File

@@ -10,13 +10,13 @@ from datetime import date, datetime, timedelta
from html import escape
from itertools import chain
from math import isnan
from typing import Any, Callable, Dict, List, Optional, Union, cast
from typing import Any, Callable, Dict, List, Optional, Union
import arrow
from tabulate import tabulate
from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ParseMode,
ReplyKeyboardMarkup, Update)
from telegram.error import NetworkError, TelegramError
from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton,
ParseMode, ReplyKeyboardMarkup, Update)
from telegram.error import BadRequest, NetworkError, TelegramError
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
from telegram.utils.helpers import escape_markdown
@@ -47,9 +47,13 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
update = kwargs.get('update') or args[0]
# Reject unauthorized messages
chat_id = int(self._config['telegram']['chat_id'])
if update.callback_query:
cchat_id = int(update.callback_query.message.chat.id)
else:
cchat_id = int(update.message.chat_id)
if int(update.message.chat_id) != chat_id:
chat_id = int(self._config['telegram']['chat_id'])
if cchat_id != chat_id:
logger.info(
'Rejected unauthorized message from: %s',
update.message.chat_id
@@ -91,7 +95,7 @@ class Telegram(RPCHandler):
Validates the keyboard configuration from telegram config
section.
"""
self._keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = [
self._keyboard: List[List[Union[str, KeyboardButton]]] = [
['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'],
['/count', '/start', '/stop', '/help']
@@ -164,8 +168,21 @@ class Telegram(RPCHandler):
CommandHandler('help', self._help),
CommandHandler('version', self._version),
]
callbacks = [
CallbackQueryHandler(self._status_table, pattern='update_status_table'),
CallbackQueryHandler(self._daily, pattern='update_daily'),
CallbackQueryHandler(self._profit, pattern='update_profit'),
CallbackQueryHandler(self._balance, pattern='update_balance'),
CallbackQueryHandler(self._performance, pattern='update_performance'),
CallbackQueryHandler(self._count, pattern='update_count'),
CallbackQueryHandler(self._forcebuy_inline),
]
for handle in handles:
self._updater.dispatcher.add_handler(handle)
for callback in callbacks:
self._updater.dispatcher.add_handler(callback)
self._updater.start_polling(
bootstrap_retries=-1,
timeout=30,
@@ -177,11 +194,6 @@ class Telegram(RPCHandler):
[h.command for h in handles]
)
self._current_callback_query_handler: Optional[CallbackQueryHandler] = None
self._callback_query_handlers = {
'forcebuy': CallbackQueryHandler(self._forcebuy_inline)
}
def cleanup(self) -> None:
"""
Stops all running telegram threads.
@@ -409,7 +421,9 @@ class Telegram(RPCHandler):
# insert separators line between Total
lines = message.split("\n")
message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_status_table",
query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@@ -447,7 +461,8 @@ class Telegram(RPCHandler):
],
tablefmt='simple')
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
self._send_msg(message, parse_mode=ParseMode.HTML)
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path="update_daily", query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@@ -519,7 +534,8 @@ class Telegram(RPCHandler):
if stats['closed_trade_count'] > 0:
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
self._send_msg(markdown_msg)
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
query=update.callback_query)
@authorized_only
def _stats(self, update: Update, context: CallbackContext) -> None:
@@ -606,7 +622,8 @@ class Telegram(RPCHandler):
f"\t`{result['stake']}: {result['total']: .8f}`\n"
f"\t`{result['symbol']}: "
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
self._send_msg(output)
self._send_msg(output, reload_able=True, callback_path="update_balance",
query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@@ -713,10 +730,10 @@ class Telegram(RPCHandler):
self._forcebuy_action(pair, price)
else:
whitelist = self._rpc._rpc_whitelist()['whitelist']
pairs = [InlineKeyboardButton(pair, callback_data=pair) for pair in whitelist]
self._send_inline_msg("Which pair?",
keyboard=self._layout_inline_keyboard(pairs),
callback_query_handler='forcebuy')
pairs = [InlineKeyboardButton(text=pair, callback_data=pair) for pair in whitelist]
self._send_msg(msg="Which pair?",
keyboard=self._layout_inline_keyboard(pairs))
@authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None:
@@ -800,7 +817,9 @@ class Telegram(RPCHandler):
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML)
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_performance",
query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@@ -820,7 +839,9 @@ class Telegram(RPCHandler):
tablefmt='simple')
message = "<pre>{}</pre>".format(message)
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
self._send_msg(message, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_count",
query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@@ -1052,29 +1073,42 @@ class Telegram(RPCHandler):
f"*Current state:* `{val['state']}`"
)
def _send_inline_msg(self, msg: str, callback_query_handler,
parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False,
keyboard: List[List[InlineKeyboardButton]] = None, ) -> None:
"""
Send given markdown message
:param msg: message
:param bot: alternative bot
:param parse_mode: telegram parse mode
:return: None
"""
if self._current_callback_query_handler:
self._updater.dispatcher.remove_handler(self._current_callback_query_handler)
self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler]
self._updater.dispatcher.add_handler(self._current_callback_query_handler)
def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "",
reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None:
if reload_able:
reply_markup = InlineKeyboardMarkup([
[InlineKeyboardButton("Refresh", callback_data=callback_path)],
])
else:
reply_markup = InlineKeyboardMarkup([[]])
msg += "\nUpdated: {}".format(datetime.now().ctime())
if not query.message:
return
chat_id = query.message.chat_id
message_id = query.message.message_id
self._send_msg(msg, parse_mode, disable_notification,
cast(List[List[Union[str, KeyboardButton, InlineKeyboardButton]]], keyboard),
reply_markup=InlineKeyboardMarkup)
try:
self._updater.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=msg,
parse_mode=parse_mode,
reply_markup=reply_markup
)
except BadRequest as e:
if 'not modified' in e.message.lower():
pass
else:
logger.warning('TelegramError: %s', e.message)
except TelegramError as telegram_err:
logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message)
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
disable_notification: bool = False,
keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = None,
reply_markup=ReplyKeyboardMarkup) -> None:
keyboard: List[List[InlineKeyboardButton]] = None,
callback_path: str = "",
reload_able: bool = False,
query: Optional[CallbackQuery] = None) -> None:
"""
Send given markdown message
:param msg: message
@@ -1082,9 +1116,19 @@ class Telegram(RPCHandler):
:param parse_mode: telegram parse mode
:return: None
"""
if keyboard is None:
keyboard = self._keyboard
reply_markup = reply_markup(keyboard, resize_keyboard=True)
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]
if query:
self._update_msg(query=query, msg=msg, parse_mode=parse_mode,
callback_path=callback_path, reload_able=reload_able)
return
if reload_able and self._config['telegram'].get('reload', True):
reply_markup = InlineKeyboardMarkup([
[InlineKeyboardButton("Refresh", callback_data=callback_path)]])
else:
if keyboard is not None:
reply_markup = InlineKeyboardMarkup(keyboard, resize_keyboard=True)
else:
reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True)
try:
try:
self._updater.bot.send_message(

View File

@@ -107,7 +107,7 @@ class IStrategy(ABC, HyperStrategyMixin):
startup_candle_count: int = 0
# Protections
protections: List
protections: List = []
# Class level variables (intentional) containing
# the dataprovider (dp) (access to other candles, historic data, ...)
@@ -453,18 +453,25 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
Ensure dataframe (length, last candle) was not modified, and has all elements we need.
"""
message_template = "Dataframe returned from strategy has mismatching {}."
message = ""
if df_len != len(dataframe):
message = "length"
if dataframe is None:
message = "No dataframe returned (return statement missing?)."
elif 'buy' not in dataframe:
message = "Buy column not set."
elif 'sell' not in dataframe:
message = "Sell column not set."
elif df_len != len(dataframe):
message = message_template.format("length")
elif df_close != dataframe["close"].iloc[-1]:
message = "last close price"
message = message_template.format("last close price")
elif df_date != dataframe["date"].iloc[-1]:
message = "last date"
message = message_template.format("last date")
if message:
if self.disable_dataframe_checks:
logger.warning(f"Dataframe returned from strategy has mismatching {message}.")
logger.warning(message)
else:
raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
raise StrategyError(message)
def get_signal(self, pair: str, timeframe: str, dataframe: DataFrame) -> Tuple[bool, bool]:
"""
@@ -524,15 +531,14 @@ class IStrategy(ABC, HyperStrategyMixin):
:param force_stoploss: Externally provided stoploss
:return: True if trade should be sold, False otherwise
"""
# Set current rate to low for backtesting sell
current_rate = low or rate
current_rate = rate
current_profit = trade.calc_profit_ratio(current_rate)
trade.adjust_min_max_rates(high or current_rate)
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=date, current_profit=current_profit,
force_stoploss=force_stoploss, high=high)
force_stoploss=force_stoploss, low=low, high=high)
# Set current rate to high for backtesting sell
current_rate = high or rate
@@ -599,18 +605,21 @@ class IStrategy(ABC, HyperStrategyMixin):
def stop_loss_reached(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float,
force_stoploss: float, high: float = None) -> SellCheckTuple:
force_stoploss: float, low: float = None,
high: float = None) -> SellCheckTuple:
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not
:param current_profit: current profit as ratio
:param low: Low value of this candle, only set in backtesting
:param high: High value of this candle, only set in backtesting
"""
stop_loss_value = force_stoploss if force_stoploss else self.stoploss
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
if self.use_custom_stoploss:
if self.use_custom_stoploss and trade.stop_loss < (low or current_rate):
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None
)(pair=trade.pair, trade=trade,
current_time=current_time,
@@ -623,7 +632,7 @@ class IStrategy(ABC, HyperStrategyMixin):
else:
logger.warning("CustomStoploss function did not return valid stoploss")
if self.trailing_stop:
if self.trailing_stop and trade.stop_loss < (low or current_rate):
# trailing stoploss handling
sl_offset = self.trailing_stop_positive_offset
@@ -643,7 +652,7 @@ class IStrategy(ABC, HyperStrategyMixin):
# evaluate if the stoploss was hit if stoploss is not on exchange
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
# regular stoploss handling.
if ((trade.stop_loss >= current_rate) and
if ((trade.stop_loss >= (low or current_rate)) and
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
sell_type = SellType.STOP_LOSS
@@ -652,7 +661,7 @@ class IStrategy(ABC, HyperStrategyMixin):
if trade.initial_stop_loss != trade.stop_loss:
sell_type = SellType.TRAILING_STOP_LOSS
logger.debug(
f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, "
f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, "
f"stoploss is {trade.stop_loss:.6f}, "
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}")