Merge remote-tracking branch 'origin/strategy_utils' into strategy_utils
This commit is contained in:
commit
da44b39423
@ -27,11 +27,10 @@ class Bybit(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 200,
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": False,
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"ohlcv_candle_limit": 200,
|
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
"mark_ohlcv_timeframe": "4h",
|
"mark_ohlcv_timeframe": "4h",
|
||||||
"funding_fee_timeframe": "8h",
|
"funding_fee_timeframe": "8h",
|
||||||
|
@ -120,8 +120,9 @@ class Order(ModelBase):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|
||||||
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
|
return (f"Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, "
|
||||||
f'side={self.side}, order_type={self.order_type}, status={self.status})')
|
f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, "
|
||||||
|
f"order_type={self.order_type}, status={self.status})")
|
||||||
|
|
||||||
def update_from_ccxt_object(self, order):
|
def update_from_ccxt_object(self, order):
|
||||||
"""
|
"""
|
||||||
@ -518,6 +519,8 @@ class LocalTrade():
|
|||||||
'close_timestamp': int(self.close_date.replace(
|
'close_timestamp': int(self.close_date.replace(
|
||||||
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
|
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
|
||||||
'realized_profit': self.realized_profit or 0.0,
|
'realized_profit': self.realized_profit or 0.0,
|
||||||
|
# Close-profit corresponds to relative realized_profit ratio
|
||||||
|
'realized_profit_ratio': self.close_profit or None,
|
||||||
'close_rate': self.close_rate,
|
'close_rate': self.close_rate,
|
||||||
'close_rate_requested': self.close_rate_requested,
|
'close_rate_requested': self.close_rate_requested,
|
||||||
'close_profit': self.close_profit, # Deprecated
|
'close_profit': self.close_profit, # Deprecated
|
||||||
|
@ -250,6 +250,7 @@ class TradeSchema(BaseModel):
|
|||||||
profit_fiat: Optional[float]
|
profit_fiat: Optional[float]
|
||||||
|
|
||||||
realized_profit: float
|
realized_profit: float
|
||||||
|
realized_profit_ratio: Optional[float]
|
||||||
|
|
||||||
exit_reason: Optional[str]
|
exit_reason: Optional[str]
|
||||||
exit_order_status: Optional[str]
|
exit_order_status: Optional[str]
|
||||||
|
@ -321,31 +321,33 @@ class Telegram(RPCHandler):
|
|||||||
and self._rpc._fiat_converter):
|
and self._rpc._fiat_converter):
|
||||||
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
msg['profit_extra'] = (
|
msg['profit_extra'] = f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}"
|
||||||
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}")
|
|
||||||
else:
|
else:
|
||||||
msg['profit_extra'] = ''
|
msg['profit_extra'] = ''
|
||||||
msg['profit_extra'] = (
|
msg['profit_extra'] = (
|
||||||
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
|
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
|
||||||
f"{msg['profit_extra']})")
|
f"{msg['profit_extra']})")
|
||||||
|
|
||||||
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
|
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
|
||||||
is_sub_trade = msg.get('sub_trade')
|
is_sub_trade = msg.get('sub_trade')
|
||||||
is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit')
|
is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit')
|
||||||
profit_prefix = ('Sub ' if is_sub_profit
|
profit_prefix = ('Sub ' if is_sub_profit else 'Cumulative ') if is_sub_trade else ''
|
||||||
else 'Cumulative ') if is_sub_trade else ''
|
|
||||||
cp_extra = ''
|
cp_extra = ''
|
||||||
|
exit_wording = 'Exited' if is_fill else 'Exiting'
|
||||||
if is_sub_profit and is_sub_trade:
|
if is_sub_profit and is_sub_trade:
|
||||||
if self._rpc._fiat_converter:
|
if self._rpc._fiat_converter:
|
||||||
cp_fiat = self._rpc._fiat_converter.convert_amount(
|
cp_fiat = self._rpc._fiat_converter.convert_amount(
|
||||||
msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency'])
|
msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}"
|
cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}"
|
||||||
else:
|
exit_wording = f"Partially {exit_wording.lower()}"
|
||||||
cp_extra = ''
|
cp_extra = (
|
||||||
cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \
|
f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} "
|
||||||
f"{msg['stake_currency']}{cp_extra}`)\n"
|
f"{msg['stake_currency']}{cp_extra}`)\n"
|
||||||
|
)
|
||||||
|
|
||||||
message = (
|
message = (
|
||||||
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
|
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
|
||||||
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
|
f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n"
|
||||||
f"{self._add_analyzed_candle(msg['pair'])}"
|
f"{self._add_analyzed_candle(msg['pair'])}"
|
||||||
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
|
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
|
||||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||||
@ -364,7 +366,7 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
||||||
message += f"*Exit Rate:* `{msg['close_rate']:.8f}`"
|
message += f"*Exit Rate:* `{msg['close_rate']:.8f}`"
|
||||||
if msg.get('sub_trade'):
|
if is_sub_trade:
|
||||||
if self._rpc._fiat_converter:
|
if self._rpc._fiat_converter:
|
||||||
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
@ -486,7 +488,9 @@ class Telegram(RPCHandler):
|
|||||||
if order_nr == 1:
|
if order_nr == 1:
|
||||||
lines.append(f"*{wording} #{order_nr}:*")
|
lines.append(f"*{wording} #{order_nr}:*")
|
||||||
lines.append(
|
lines.append(
|
||||||
f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
f"*Amount:* {cur_entry_amount} "
|
||||||
|
f"({round_coin_value(order['cost'], quote_currency)})"
|
||||||
|
)
|
||||||
lines.append(f"*Average Price:* {cur_entry_average}")
|
lines.append(f"*Average Price:* {cur_entry_average}")
|
||||||
else:
|
else:
|
||||||
sum_stake = 0
|
sum_stake = 0
|
||||||
@ -582,7 +586,7 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
if position_adjust:
|
if position_adjust:
|
||||||
max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "")
|
max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "")
|
||||||
lines.append("*Number of Entries:* `{num_entries}`" + max_buy_str)
|
lines.append("*Number of Entries:* `{num_entries}" + max_buy_str + "`")
|
||||||
lines.append("*Number of Exits:* `{num_exits}`")
|
lines.append("*Number of Exits:* `{num_exits}`")
|
||||||
|
|
||||||
lines.extend([
|
lines.extend([
|
||||||
@ -597,7 +601,8 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
if r['is_open']:
|
if r['is_open']:
|
||||||
if r.get('realized_profit'):
|
if r.get('realized_profit'):
|
||||||
lines.append("*Realized Profit:* `{realized_profit_r}`")
|
lines.append(
|
||||||
|
"*Realized Profit:* `{realized_profit_r} {realized_profit_ratio:.2%}`")
|
||||||
lines.append("*Total Profit:* `{total_profit_abs_r}` ")
|
lines.append("*Total Profit:* `{total_profit_abs_r}` ")
|
||||||
|
|
||||||
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
|
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
|
||||||
|
@ -463,7 +463,9 @@ class TestCCXTExchange():
|
|||||||
if exchangename == 'gate':
|
if exchangename == 'gate':
|
||||||
# TODO: Gate is unstable here at the moment, ignoring the limit partially.
|
# TODO: Gate is unstable here at the moment, ignoring the limit partially.
|
||||||
return
|
return
|
||||||
for val in [1, 2, 5, 25, 100]:
|
for val in [1, 2, 5, 25, 50, 100]:
|
||||||
|
if val > 50 and exchangename == 'bybit':
|
||||||
|
continue
|
||||||
l2 = exch.fetch_l2_order_book(pair, val)
|
l2 = exch.fetch_l2_order_book(pair, val)
|
||||||
if not l2_limit_range or val in l2_limit_range:
|
if not l2_limit_range or val in l2_limit_range:
|
||||||
if val > 50:
|
if val > 50:
|
||||||
|
@ -1362,6 +1362,7 @@ def test_to_json(fee):
|
|||||||
'trade_duration': None,
|
'trade_duration': None,
|
||||||
'trade_duration_s': None,
|
'trade_duration_s': None,
|
||||||
'realized_profit': 0.0,
|
'realized_profit': 0.0,
|
||||||
|
'realized_profit_ratio': None,
|
||||||
'close_profit': None,
|
'close_profit': None,
|
||||||
'close_profit_pct': None,
|
'close_profit_pct': None,
|
||||||
'close_profit_abs': None,
|
'close_profit_abs': None,
|
||||||
@ -1438,6 +1439,7 @@ def test_to_json(fee):
|
|||||||
'initial_stop_loss_pct': None,
|
'initial_stop_loss_pct': None,
|
||||||
'initial_stop_loss_ratio': None,
|
'initial_stop_loss_ratio': None,
|
||||||
'realized_profit': 0.0,
|
'realized_profit': 0.0,
|
||||||
|
'realized_profit_ratio': None,
|
||||||
'close_profit': None,
|
'close_profit': None,
|
||||||
'close_profit_pct': None,
|
'close_profit_pct': None,
|
||||||
'close_profit_abs': None,
|
'close_profit_abs': None,
|
||||||
|
@ -76,6 +76,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'stoploss_entry_dist_ratio': -0.10376381,
|
'stoploss_entry_dist_ratio': -0.10376381,
|
||||||
'open_order': None,
|
'open_order': None,
|
||||||
'realized_profit': 0.0,
|
'realized_profit': 0.0,
|
||||||
|
'realized_profit_ratio': None,
|
||||||
'total_profit_abs': -4.09e-06,
|
'total_profit_abs': -4.09e-06,
|
||||||
'total_profit_fiat': ANY,
|
'total_profit_fiat': ANY,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
|
@ -1013,6 +1013,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
|||||||
'total_profit_abs': ANY,
|
'total_profit_abs': ANY,
|
||||||
'total_profit_fiat': ANY,
|
'total_profit_fiat': ANY,
|
||||||
'realized_profit': 0.0,
|
'realized_profit': 0.0,
|
||||||
|
'realized_profit_ratio': None,
|
||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_timestamp': ANY,
|
'open_timestamp': ANY,
|
||||||
@ -1243,6 +1244,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
|||||||
'profit_abs': None,
|
'profit_abs': None,
|
||||||
'profit_fiat': None,
|
'profit_fiat': None,
|
||||||
'realized_profit': 0.0,
|
'realized_profit': 0.0,
|
||||||
|
'realized_profit_ratio': None,
|
||||||
'fee_close': 0.0025,
|
'fee_close': 0.0025,
|
||||||
'fee_close_cost': None,
|
'fee_close_cost': None,
|
||||||
'fee_close_currency': None,
|
'fee_close_currency': None,
|
||||||
|
@ -2012,7 +2012,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
'sub_trade': True,
|
'sub_trade': True,
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
'\N{WARNING SIGN} *Binance (dry):* Partially exiting KEY/ETH (#1)\n'
|
||||||
'*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
'*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
||||||
'*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n'
|
'*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n'
|
||||||
'*Enter Tag:* `buy_signal1`\n'
|
'*Enter Tag:* `buy_signal1`\n'
|
||||||
|
@ -169,6 +169,36 @@ def test_stoploss_from_open(side, profitrange):
|
|||||||
assert pytest.approx(stop_price) == expected_stop_price
|
assert pytest.approx(stop_price) == expected_stop_price
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("side,rel_stop,curr_profit,leverage,expected", [
|
||||||
|
# profit range for long is [-1, inf] while for shorts is [-inf, 1]
|
||||||
|
("long", 0, -1, 1, 1),
|
||||||
|
("long", 0, 0.1, 1, 0.09090909),
|
||||||
|
("long", -0.1, 0.1, 1, 0.18181818),
|
||||||
|
("long", 0.1, 0.2, 1, 0.08333333),
|
||||||
|
("long", 0.1, 0.5, 1, 0.266666666),
|
||||||
|
("long", 0.1, 5, 1, 0.816666666), # 500% profit, set stoploss to 10% above open price
|
||||||
|
|
||||||
|
("short", 0, 0.1, 1, 0.1111111),
|
||||||
|
("short", -0.1, 0.1, 1, 0.2222222),
|
||||||
|
("short", 0.1, 0.2, 1, 0.125),
|
||||||
|
("short", 0.1, 1, 1, 1),
|
||||||
|
])
|
||||||
|
def test_stoploss_from_open_leverage(side, rel_stop, curr_profit, leverage, expected):
|
||||||
|
|
||||||
|
stoploss = stoploss_from_open(rel_stop, curr_profit, side == 'short')
|
||||||
|
assert pytest.approx(stoploss) == expected
|
||||||
|
open_rate = 100
|
||||||
|
if stoploss != 1:
|
||||||
|
if side == 'long':
|
||||||
|
current_rate = open_rate * (1 + curr_profit)
|
||||||
|
stop = current_rate * (1 - stoploss)
|
||||||
|
assert pytest.approx(stop) == open_rate * (1 + rel_stop)
|
||||||
|
else:
|
||||||
|
current_rate = open_rate * (1 - curr_profit)
|
||||||
|
stop = current_rate * (1 + stoploss)
|
||||||
|
assert pytest.approx(stop) == open_rate * (1 - rel_stop)
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_from_absolute():
|
def test_stoploss_from_absolute():
|
||||||
assert pytest.approx(stoploss_from_absolute(90, 100)) == 1 - (90 / 100)
|
assert pytest.approx(stoploss_from_absolute(90, 100)) == 1 - (90 / 100)
|
||||||
assert pytest.approx(stoploss_from_absolute(90, 100)) == 0.1
|
assert pytest.approx(stoploss_from_absolute(90, 100)) == 0.1
|
||||||
|
Loading…
Reference in New Issue
Block a user