Merge pull request #3609 from thopd88/develop
Add telegram /trades command
This commit is contained in:
commit
db8f3a9e9b
@ -56,6 +56,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/show_config` | | Shows part of the current configuration with relevant settings to operation
|
| `/show_config` | | Shows part of the current configuration with relevant settings to operation
|
||||||
| `/status` | | Lists all open trades
|
| `/status` | | Lists all open trades
|
||||||
| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
||||||
|
| `/trades [limit]` | | List all recently closed trades in a table format.
|
||||||
| `/count` | | Displays number of trades used and available
|
| `/count` | | Displays number of trades used and available
|
||||||
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||||
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
|
@ -252,9 +252,10 @@ class RPC:
|
|||||||
def _rpc_trade_history(self, limit: int) -> Dict:
|
def _rpc_trade_history(self, limit: int) -> Dict:
|
||||||
""" Returns the X last trades """
|
""" Returns the X last trades """
|
||||||
if limit > 0:
|
if limit > 0:
|
||||||
trades = Trade.get_trades().order_by(Trade.id.desc()).limit(limit)
|
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
|
||||||
|
Trade.id.desc()).limit(limit)
|
||||||
else:
|
else:
|
||||||
trades = Trade.get_trades().order_by(Trade.id.desc()).all()
|
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all()
|
||||||
|
|
||||||
output = [trade.to_json() for trade in trades]
|
output = [trade.to_json() for trade in trades]
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ This module manage Telegram communication
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import arrow
|
||||||
from typing import Any, Callable, Dict
|
from typing import Any, Callable, Dict
|
||||||
|
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
@ -92,6 +93,7 @@ class Telegram(RPC):
|
|||||||
CommandHandler('stop', self._stop),
|
CommandHandler('stop', self._stop),
|
||||||
CommandHandler('forcesell', self._forcesell),
|
CommandHandler('forcesell', self._forcesell),
|
||||||
CommandHandler('forcebuy', self._forcebuy),
|
CommandHandler('forcebuy', self._forcebuy),
|
||||||
|
CommandHandler('trades', self._trades),
|
||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
CommandHandler('daily', self._daily),
|
CommandHandler('daily', self._daily),
|
||||||
CommandHandler('count', self._count),
|
CommandHandler('count', self._count),
|
||||||
@ -496,6 +498,41 @@ class Telegram(RPC):
|
|||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _trades(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /trades <n>
|
||||||
|
Returns last n recent trades.
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
stake_cur = self._config['stake_currency']
|
||||||
|
try:
|
||||||
|
nrecent = int(context.args[0])
|
||||||
|
except (TypeError, ValueError, IndexError):
|
||||||
|
nrecent = 10
|
||||||
|
try:
|
||||||
|
trades = self._rpc_trade_history(
|
||||||
|
nrecent
|
||||||
|
)
|
||||||
|
trades_tab = tabulate(
|
||||||
|
[[arrow.get(trade['open_date']).humanize(),
|
||||||
|
trade['pair'],
|
||||||
|
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
|
||||||
|
for trade in trades['trades']],
|
||||||
|
headers=[
|
||||||
|
'Open Date',
|
||||||
|
'Pair',
|
||||||
|
f'Profit ({stake_cur})',
|
||||||
|
],
|
||||||
|
tablefmt='simple')
|
||||||
|
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
|
||||||
|
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
|
||||||
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _performance(self, update: Update, context: CallbackContext) -> None:
|
def _performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -609,6 +646,7 @@ class Telegram(RPC):
|
|||||||
" *table :* `will display trades in a table`\n"
|
" *table :* `will display trades in a table`\n"
|
||||||
" `pending buy orders are marked with an asterisk (*)`\n"
|
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||||
" `pending sell orders are marked with a double asterisk (**)`\n"
|
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||||
|
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||||
"*/profit:* `Lists cumulative profit from all finished trades`\n"
|
"*/profit:* `Lists cumulative profit from all finished trades`\n"
|
||||||
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
||||||
"regardless of profit`\n"
|
"regardless of profit`\n"
|
||||||
|
@ -1089,7 +1089,7 @@ def test_show_trades(mocker, fee, capsys, caplog):
|
|||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
start_show_trades(pargs)
|
start_show_trades(pargs)
|
||||||
assert log_has("Printing 3 Trades: ", caplog)
|
assert log_has("Printing 4 Trades: ", caplog)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "Trade(id=1" in captured.out
|
assert "Trade(id=1" in captured.out
|
||||||
assert "Trade(id=2" in captured.out
|
assert "Trade(id=2" in captured.out
|
||||||
|
@ -199,6 +199,20 @@ def create_mock_trades(fee):
|
|||||||
)
|
)
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
|
|
||||||
|
trade = Trade(
|
||||||
|
pair='XRP/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
amount=123.0,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
open_rate=0.05,
|
||||||
|
close_rate=0.06,
|
||||||
|
close_profit=0.01,
|
||||||
|
exchange='bittrex',
|
||||||
|
is_open=False,
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
|
||||||
# Simulate prod entry
|
# Simulate prod entry
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='ETC/BTC',
|
pair='ETC/BTC',
|
||||||
|
@ -43,7 +43,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
|
|||||||
|
|
||||||
trades = load_trades_from_db(db_url=default_conf['db_url'])
|
trades = load_trades_from_db(db_url=default_conf['db_url'])
|
||||||
assert init_mock.call_count == 1
|
assert init_mock.call_count == 1
|
||||||
assert len(trades) == 3
|
assert len(trades) == 4
|
||||||
assert isinstance(trades, DataFrame)
|
assert isinstance(trades, DataFrame)
|
||||||
assert "pair" in trades.columns
|
assert "pair" in trades.columns
|
||||||
assert "open_time" in trades.columns
|
assert "open_time" in trades.columns
|
||||||
|
@ -284,12 +284,11 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
|
|||||||
assert isinstance(trades['trades'][1], dict)
|
assert isinstance(trades['trades'][1], dict)
|
||||||
|
|
||||||
trades = rpc._rpc_trade_history(0)
|
trades = rpc._rpc_trade_history(0)
|
||||||
assert len(trades['trades']) == 3
|
assert len(trades['trades']) == 2
|
||||||
assert trades['trades_count'] == 3
|
assert trades['trades_count'] == 2
|
||||||
# The first trade is for ETH ... sorting is descending
|
# The first closed trade is for ETC ... sorting is descending
|
||||||
assert trades['trades'][-1]['pair'] == 'ETH/BTC'
|
assert trades['trades'][-1]['pair'] == 'ETC/BTC'
|
||||||
assert trades['trades'][0]['pair'] == 'ETC/BTC'
|
assert trades['trades'][0]['pair'] == 'XRP/BTC'
|
||||||
assert trades['trades'][1]['pair'] == 'ETC/BTC'
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
|
@ -368,12 +368,12 @@ def test_api_trades(botclient, mocker, ticker, fee, markets):
|
|||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/trades")
|
rc = client_get(client, f"{BASE_URI}/trades")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert len(rc.json['trades']) == 3
|
|
||||||
assert rc.json['trades_count'] == 3
|
|
||||||
rc = client_get(client, f"{BASE_URI}/trades?limit=2")
|
|
||||||
assert_response(rc)
|
|
||||||
assert len(rc.json['trades']) == 2
|
assert len(rc.json['trades']) == 2
|
||||||
assert rc.json['trades_count'] == 2
|
assert rc.json['trades_count'] == 2
|
||||||
|
rc = client_get(client, f"{BASE_URI}/trades?limit=1")
|
||||||
|
assert_response(rc)
|
||||||
|
assert len(rc.json['trades']) == 1
|
||||||
|
assert rc.json['trades_count'] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
||||||
|
@ -21,8 +21,9 @@ from freqtrade.rpc import RPCMessageType
|
|||||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange,
|
from tests.conftest import (create_mock_trades, get_patched_freqtradebot,
|
||||||
patch_get_signal, patch_whitelist)
|
log_has, patch_exchange, patch_get_signal,
|
||||||
|
patch_whitelist)
|
||||||
|
|
||||||
|
|
||||||
class DummyCls(Telegram):
|
class DummyCls(Telegram):
|
||||||
@ -60,7 +61,7 @@ def test__init__(default_conf, mocker) -> None:
|
|||||||
assert telegram._config == default_conf
|
assert telegram._config == default_conf
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker, caplog) -> None:
|
def test_telegram_init(default_conf, mocker, caplog) -> None:
|
||||||
start_polling = MagicMock()
|
start_polling = MagicMock()
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ def test_init(default_conf, mocker, caplog) -> None:
|
|||||||
assert start_polling.start_polling.call_count == 1
|
assert start_polling.start_polling.call_count == 1
|
||||||
|
|
||||||
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], "
|
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
|
||||||
"['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], "
|
"['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], "
|
||||||
"['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], "
|
"['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], "
|
||||||
"['edge'], ['help'], ['version']]")
|
"['edge'], ['help'], ['version']]")
|
||||||
@ -1146,6 +1147,36 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
|
|||||||
assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0]
|
assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_trades(mocker, update, default_conf, fee):
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
context = MagicMock()
|
||||||
|
context.args = []
|
||||||
|
|
||||||
|
telegram._trades(update=update, context=context)
|
||||||
|
assert "<b>0 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert "<pre>" not in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
create_mock_trades(fee)
|
||||||
|
|
||||||
|
context = MagicMock()
|
||||||
|
context.args = [5]
|
||||||
|
telegram._trades(update=update, context=context)
|
||||||
|
msg_mock.call_count == 1
|
||||||
|
assert "2 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert "Profit (" in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert "Open Date" in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert "<pre>" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_help_handle(default_conf, update, mocker) -> None:
|
def test_help_handle(default_conf, update, mocker) -> None:
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
@ -4094,7 +4094,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
|
|||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
trades = Trade.query.all()
|
trades = Trade.query.all()
|
||||||
assert len(trades) == 3
|
assert len(trades) == 4
|
||||||
freqtrade.cancel_all_open_orders()
|
freqtrade.cancel_all_open_orders()
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
assert sell_mock.call_count == 1
|
assert sell_mock.call_count == 1
|
||||||
|
@ -989,7 +989,7 @@ def test_get_overall_performance(fee):
|
|||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
res = Trade.get_overall_performance()
|
res = Trade.get_overall_performance()
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 2
|
||||||
assert 'pair' in res[0]
|
assert 'pair' in res[0]
|
||||||
assert 'profit' in res[0]
|
assert 'profit' in res[0]
|
||||||
assert 'count' in res[0]
|
assert 'count' in res[0]
|
||||||
@ -1004,5 +1004,5 @@ def test_get_best_pair(fee):
|
|||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
res = Trade.get_best_pair()
|
res = Trade.get_best_pair()
|
||||||
assert len(res) == 2
|
assert len(res) == 2
|
||||||
assert res[0] == 'ETC/BTC'
|
assert res[0] == 'XRP/BTC'
|
||||||
assert res[1] == 0.005
|
assert res[1] == 0.01
|
||||||
|
Loading…
Reference in New Issue
Block a user