Merge branch 'freqtrade:develop' into rootless
This commit is contained in:
commit
d0c589a78c
@ -106,15 +106,18 @@ class Ftx(Exchange):
|
|||||||
if order[0].get('status') == 'closed':
|
if order[0].get('status') == 'closed':
|
||||||
# Trigger order was triggered ...
|
# Trigger order was triggered ...
|
||||||
real_order_id = order[0].get('info', {}).get('orderId')
|
real_order_id = order[0].get('info', {}).get('orderId')
|
||||||
|
# OrderId may be None for stoploss-market orders
|
||||||
|
# But contains "average" in these cases.
|
||||||
|
if real_order_id:
|
||||||
|
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
|
||||||
|
order1['type'] = 'stop'
|
||||||
|
order1['status_stop'] = 'triggered'
|
||||||
|
return order1
|
||||||
|
|
||||||
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
|
|
||||||
order1['type'] = 'stop'
|
|
||||||
order1['status_stop'] = 'triggered'
|
|
||||||
return order1
|
|
||||||
return order[0]
|
return order[0]
|
||||||
else:
|
else:
|
||||||
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
|
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
|
||||||
|
@ -1021,12 +1021,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# Cancelled orders may have the status of 'canceled' or 'closed'
|
# Cancelled orders may have the status of 'canceled' or 'closed'
|
||||||
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
filled_val = order.get('filled', 0.0) or 0.0
|
filled_val: float = order.get('filled', 0.0) or 0.0
|
||||||
filled_stake = filled_val * trade.open_rate
|
filled_stake = filled_val * trade.open_rate
|
||||||
minstake = self.exchange.get_min_pair_stake_amount(
|
minstake = self.exchange.get_min_pair_stake_amount(
|
||||||
trade.pair, trade.open_rate, self.strategy.stoploss)
|
trade.pair, trade.open_rate, self.strategy.stoploss)
|
||||||
|
|
||||||
if filled_val > 0 and filled_stake < minstake:
|
if filled_val > 0 and minstake and filled_stake < minstake:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
|
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
|
||||||
f"as the filled amount of {filled_val} would result in an unsellable trade.")
|
f"as the filled amount of {filled_val} would result in an unsellable trade.")
|
||||||
|
@ -29,18 +29,23 @@ def decimals_per_coin(coin: str):
|
|||||||
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK)
|
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK)
|
||||||
|
|
||||||
|
|
||||||
def round_coin_value(value: float, coin: str, show_coin_name=True) -> str:
|
def round_coin_value(
|
||||||
|
value: float, coin: str, show_coin_name=True, keep_trailing_zeros=False) -> str:
|
||||||
"""
|
"""
|
||||||
Get price value for this coin
|
Get price value for this coin
|
||||||
:param value: Value to be printed
|
:param value: Value to be printed
|
||||||
:param coin: Which coin are we printing the price / value for
|
:param coin: Which coin are we printing the price / value for
|
||||||
:param show_coin_name: Return string in format: "222.22 USDT" or "222.22"
|
:param show_coin_name: Return string in format: "222.22 USDT" or "222.22"
|
||||||
|
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
|
||||||
:return: Formatted / rounded value (with or without coin name)
|
:return: Formatted / rounded value (with or without coin name)
|
||||||
"""
|
"""
|
||||||
|
val = f"{value:.{decimals_per_coin(coin)}f}"
|
||||||
|
if not keep_trailing_zeros:
|
||||||
|
val = val.rstrip('0').rstrip('.')
|
||||||
if show_coin_name:
|
if show_coin_name:
|
||||||
return f"{value:.{decimals_per_coin(coin)}f} {coin}"
|
val = f"{val} {coin}"
|
||||||
else:
|
|
||||||
return f"{value:.{decimals_per_coin(coin)}f}"
|
return val
|
||||||
|
|
||||||
|
|
||||||
def shorten_date(_date: str) -> str:
|
def shorten_date(_date: str) -> str:
|
||||||
|
@ -373,7 +373,7 @@ class HyperoptTools():
|
|||||||
|
|
||||||
trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
|
trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
|
||||||
lambda x: "{} {}".format(
|
lambda x: "{} {}".format(
|
||||||
round_coin_value(x['max_drawdown_abs'], stake_currency),
|
round_coin_value(x['max_drawdown_abs'], stake_currency, keep_trailing_zeros=True),
|
||||||
(f"({x['max_drawdown_account']:,.2%})"
|
(f"({x['max_drawdown_account']:,.2%})"
|
||||||
if has_account_drawdown
|
if has_account_drawdown
|
||||||
else f"({x['max_drawdown']:,.2%})"
|
else f"({x['max_drawdown']:,.2%})"
|
||||||
@ -388,7 +388,7 @@ class HyperoptTools():
|
|||||||
|
|
||||||
trials['Profit'] = trials.apply(
|
trials['Profit'] = trials.apply(
|
||||||
lambda x: '{} {}'.format(
|
lambda x: '{} {}'.format(
|
||||||
round_coin_value(x['Total profit'], stake_currency),
|
round_coin_value(x['Total profit'], stake_currency, keep_trailing_zeros=True),
|
||||||
f"({x['Profit']:,.2%})".rjust(10, ' ')
|
f"({x['Profit']:,.2%})".rjust(10, ' ')
|
||||||
).rjust(25+len(stake_currency))
|
).rjust(25+len(stake_currency))
|
||||||
if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)),
|
if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)),
|
||||||
|
@ -799,11 +799,11 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
fee_close = Column(Float, nullable=False, default=0.0)
|
fee_close = Column(Float, nullable=False, default=0.0)
|
||||||
fee_close_cost = Column(Float, nullable=True)
|
fee_close_cost = Column(Float, nullable=True)
|
||||||
fee_close_currency = Column(String(25), nullable=True)
|
fee_close_currency = Column(String(25), nullable=True)
|
||||||
open_rate = Column(Float)
|
open_rate: float = Column(Float)
|
||||||
open_rate_requested = Column(Float)
|
open_rate_requested = Column(Float)
|
||||||
# open_trade_value - calculated via _calc_open_trade_value
|
# open_trade_value - calculated via _calc_open_trade_value
|
||||||
open_trade_value = Column(Float)
|
open_trade_value = Column(Float)
|
||||||
close_rate = Column(Float)
|
close_rate: Optional[float] = Column(Float)
|
||||||
close_rate_requested = Column(Float)
|
close_rate_requested = Column(Float)
|
||||||
close_profit = Column(Float)
|
close_profit = Column(Float)
|
||||||
close_profit_abs = Column(Float)
|
close_profit_abs = Column(Float)
|
||||||
|
@ -790,12 +790,13 @@ class Telegram(RPCHandler):
|
|||||||
output = ''
|
output = ''
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
output += "*Warning:* Simulated balances in Dry Mode.\n"
|
output += "*Warning:* Simulated balances in Dry Mode.\n"
|
||||||
|
starting_cap = round_coin_value(
|
||||||
output += ("Starting capital: "
|
result['starting_capital'], self._config['stake_currency'])
|
||||||
f"`{result['starting_capital']}` {self._config['stake_currency']}"
|
output += f"Starting capital: `{starting_cap}`"
|
||||||
)
|
starting_cap_fiat = round_coin_value(
|
||||||
output += (f" `{result['starting_capital_fiat']}` "
|
result['starting_capital_fiat'], self._config['fiat_display_currency']
|
||||||
f"{self._config['fiat_display_currency']}.\n"
|
) if result['starting_capital_fiat'] > 0 else ''
|
||||||
|
output += (f" `, {starting_cap_fiat}`.\n"
|
||||||
) if result['starting_capital_fiat'] > 0 else '.\n'
|
) if result['starting_capital_fiat'] > 0 else '.\n'
|
||||||
|
|
||||||
total_dust_balance = 0
|
total_dust_balance = 0
|
||||||
|
@ -125,7 +125,7 @@ def test_stoploss_adjust_ftx(mocker, default_conf):
|
|||||||
assert not exchange.stoploss_adjust(1501, order)
|
assert not exchange.stoploss_adjust(1501, order)
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):
|
def test_fetch_stoploss_order_ftx(default_conf, mocker, limit_sell_order):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
order = MagicMock()
|
order = MagicMock()
|
||||||
order.myid = 123
|
order.myid = 123
|
||||||
@ -147,9 +147,15 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):
|
|||||||
with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"):
|
with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"):
|
||||||
exchange.fetch_stoploss_order('X', 'TKN/BTC')['status']
|
exchange.fetch_stoploss_order('X', 'TKN/BTC')['status']
|
||||||
|
|
||||||
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': 'closed'}])
|
# stoploss Limit order
|
||||||
|
api_mock.fetch_orders = MagicMock(return_value=[
|
||||||
|
{'id': 'X', 'status': 'closed',
|
||||||
|
'info': {
|
||||||
|
'orderId': 'mocked_limit_sell',
|
||||||
|
}}])
|
||||||
api_mock.fetch_order = MagicMock(return_value=limit_sell_order)
|
api_mock.fetch_order = MagicMock(return_value=limit_sell_order)
|
||||||
|
|
||||||
|
# No orderId field - no call to fetch_order
|
||||||
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
|
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
|
||||||
assert resp
|
assert resp
|
||||||
assert api_mock.fetch_order.call_count == 1
|
assert api_mock.fetch_order.call_count == 1
|
||||||
@ -158,6 +164,17 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):
|
|||||||
assert resp['type'] == 'stop'
|
assert resp['type'] == 'stop'
|
||||||
assert resp['status_stop'] == 'triggered'
|
assert resp['status_stop'] == 'triggered'
|
||||||
|
|
||||||
|
# Stoploss market order
|
||||||
|
# Contains no new Order, but "average" instead
|
||||||
|
order = {'id': 'X', 'status': 'closed', 'info': {'orderId': None}, 'average': 0.254}
|
||||||
|
api_mock.fetch_orders = MagicMock(return_value=[order])
|
||||||
|
api_mock.fetch_order.reset_mock()
|
||||||
|
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
|
||||||
|
assert resp
|
||||||
|
# fetch_order not called (no regular order ID)
|
||||||
|
assert api_mock.fetch_order.call_count == 0
|
||||||
|
assert order == order
|
||||||
|
|
||||||
with pytest.raises(InvalidOrderException):
|
with pytest.raises(InvalidOrderException):
|
||||||
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
|
||||||
|
@ -770,7 +770,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
|
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01)
|
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01)
|
||||||
assert ('∙ `-0.00000500 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
assert ('∙ `-0.000005 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
in msg_mock.call_args_list[-1][0][0])
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
@ -845,7 +845,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
|
|||||||
assert '*XRP:*' not in result
|
assert '*XRP:*' not in result
|
||||||
assert 'Balance:' in result
|
assert 'Balance:' in result
|
||||||
assert 'Est. BTC:' in result
|
assert 'Est. BTC:' in result
|
||||||
assert 'BTC: 12.00000000' in result
|
assert 'BTC: 12' in result
|
||||||
assert "*3 Other Currencies (< 0.0001 BTC):*" in result
|
assert "*3 Other Currencies (< 0.0001 BTC):*" in result
|
||||||
assert 'BTC: 0.00000309' in result
|
assert 'BTC: 0.00000309' in result
|
||||||
|
|
||||||
@ -874,7 +874,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None
|
|||||||
result = msg_mock.call_args_list[0][0][0]
|
result = msg_mock.call_args_list[0][0][0]
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert "*Warning:* Simulated balances in Dry Mode." in result
|
assert "*Warning:* Simulated balances in Dry Mode." in result
|
||||||
assert "Starting capital: `1000` BTC" in result
|
assert "Starting capital: `1000 BTC`" in result
|
||||||
|
|
||||||
|
|
||||||
def test_balance_handle_too_large_response(default_conf, update, mocker) -> None:
|
def test_balance_handle_too_large_response(default_conf, update, mocker) -> None:
|
||||||
@ -1734,7 +1734,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
|
|||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'limit': 1.099e-05,
|
'limit': 1.099e-05,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.01465333,
|
||||||
'stake_amount_fiat': 0.0,
|
'stake_amount_fiat': 0.0,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
@ -1751,7 +1751,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
|
|||||||
'*Amount:* `1333.33333333`\n' \
|
'*Amount:* `1333.33333333`\n' \
|
||||||
'*Open Rate:* `0.00001099`\n' \
|
'*Open Rate:* `0.00001099`\n' \
|
||||||
'*Current Rate:* `0.00001099`\n' \
|
'*Current Rate:* `0.00001099`\n' \
|
||||||
'*Total:* `(0.00100000 BTC, 12.345 USD)`'
|
'*Total:* `(0.01465333 BTC, 180.895 USD)`'
|
||||||
|
|
||||||
freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'}
|
freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'}
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@ -1825,7 +1825,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
|
|||||||
'buy_tag': 'buy_signal_01',
|
'buy_tag': 'buy_signal_01',
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.01465333,
|
||||||
# 'stake_amount_fiat': 0.0,
|
# 'stake_amount_fiat': 0.0,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
@ -1839,7 +1839,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
|
|||||||
'*Buy Tag:* `buy_signal_01`\n' \
|
'*Buy Tag:* `buy_signal_01`\n' \
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Amount:* `1333.33333333`\n' \
|
||||||
'*Open Rate:* `0.00001099`\n' \
|
'*Open Rate:* `0.00001099`\n' \
|
||||||
'*Total:* `(0.00100000 BTC, 12.345 USD)`'
|
'*Total:* `(0.01465333 BTC, 180.895 USD)`'
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
@ -2031,7 +2031,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'limit': 1.099e-05,
|
'limit': 1.099e-05,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.01465333,
|
||||||
'stake_amount_fiat': 0.0,
|
'stake_amount_fiat': 0.0,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': None,
|
'fiat_currency': None,
|
||||||
@ -2044,7 +2044,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00001099`\n'
|
'*Open Rate:* `0.00001099`\n'
|
||||||
'*Current Rate:* `0.00001099`\n'
|
'*Current Rate:* `0.00001099`\n'
|
||||||
'*Total:* `(0.00100000 BTC)`')
|
'*Total:* `(0.01465333 BTC)`')
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
|
@ -2436,6 +2436,9 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_buy_order_
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
||||||
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
|
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
|
||||||
assert log_has_re(r"Order .* for .* not cancelled.", caplog)
|
assert log_has_re(r"Order .* for .* not cancelled.", caplog)
|
||||||
|
# min_pair_stake empty should not crash
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=None)
|
||||||
|
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
|
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
|
||||||
|
@ -21,16 +21,19 @@ def test_decimals_per_coin():
|
|||||||
|
|
||||||
def test_round_coin_value():
|
def test_round_coin_value():
|
||||||
assert round_coin_value(222.222222, 'USDT') == '222.222 USDT'
|
assert round_coin_value(222.222222, 'USDT') == '222.222 USDT'
|
||||||
assert round_coin_value(222.2, 'USDT') == '222.200 USDT'
|
assert round_coin_value(222.2, 'USDT', keep_trailing_zeros=True) == '222.200 USDT'
|
||||||
|
assert round_coin_value(222.2, 'USDT') == '222.2 USDT'
|
||||||
assert round_coin_value(222.12745, 'EUR') == '222.127 EUR'
|
assert round_coin_value(222.12745, 'EUR') == '222.127 EUR'
|
||||||
assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC'
|
assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC'
|
||||||
assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH'
|
assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH'
|
||||||
|
|
||||||
assert round_coin_value(222.222222, 'USDT', False) == '222.222'
|
assert round_coin_value(222.222222, 'USDT', False) == '222.222'
|
||||||
assert round_coin_value(222.2, 'USDT', False) == '222.200'
|
assert round_coin_value(222.2, 'USDT', False) == '222.2'
|
||||||
|
assert round_coin_value(222.00, 'USDT', False) == '222'
|
||||||
assert round_coin_value(222.12745, 'EUR', False) == '222.127'
|
assert round_coin_value(222.12745, 'EUR', False) == '222.127'
|
||||||
assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121'
|
assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121'
|
||||||
assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745'
|
assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745'
|
||||||
|
assert round_coin_value(222.2, 'USDT', False, True) == '222.200'
|
||||||
|
|
||||||
|
|
||||||
def test_shorten_date() -> None:
|
def test_shorten_date() -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user