diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 2198fd917..ba0ad1785 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -87,7 +87,8 @@ class Trade(_DECL_BASE): :param order: order retrieved by exchange.get_order() :return: None """ - if not order['closed']: + # Ignore open and cancelled orders + if not order['closed'] or order['rate'] is None: return logger.info('Updating trade (id=%d) ...', self.id) @@ -96,22 +97,28 @@ class Trade(_DECL_BASE): self.open_rate = order['rate'] self.amount = order['amount'] logger.info('LIMIT_BUY has been fulfilled for %s.', self) + self.open_order_id = None elif order['type'] == 'LIMIT_SELL': - # Set close rate and set actual profit - self.close_rate = order['rate'] - self.close_profit = self.calc_profit() - self.close_date = datetime.utcnow() - self.is_open = False - logger.info( - 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', - self - ) + self.close(order['rate']) else: raise ValueError('Unknown order type: {}'.format(order['type'])) - - self.open_order_id = None Trade.session.flush() + def close(self, rate: float) -> None: + """ + Sets close_rate to the given rate, calculates total profit + and marks trade as closed + """ + self.close_rate = rate + self.close_profit = self.calc_profit() + self.close_date = datetime.utcnow() + self.is_open = False + self.open_order_id = None + logger.info( + 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', + self + ) + def calc_profit(self, rate: Optional[float] = None) -> float: """ Calculates the profit in percentage (including fee). diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4b0465ddb..dc5dbf734 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1,10 +1,10 @@ import logging import re from datetime import timedelta, date +from decimal import Decimal from typing import Callable, Any import arrow -from decimal import Decimal from pandas import DataFrame from sqlalchemy import and_, func, text, between from tabulate import tabulate @@ -208,6 +208,7 @@ def _status_table(bot: Bot, update: Update) -> None: send_msg(message, parse_mode=ParseMode.HTML) + @authorized_only def _daily(bot: Bot, update: Update) -> None: """ @@ -217,37 +218,33 @@ def _daily(bot: Bot, update: Update) -> None: :param update: message update :return: None """ - trades = Trade.query.order_by(Trade.close_date).all() today = date.today().toordinal() profit_days = {} - + try: timescale = int(update.message.text.replace('/daily', '').strip()) - except: + except (TypeError, ValueError): timescale = 5 - + if not (isinstance(timescale, int) and timescale > 0): send_msg('*Daily [n]:* `must be an integer greater than 0`', bot=bot) return for day in range(0, timescale): - #need to query between day+1 and day-1 - nextdate = date.fromordinal(today-day+1) - prevdate = date.fromordinal(today-day-1) - trades = Trade.query.filter(between(Trade.close_date, prevdate, nextdate)).all() - curdayprofit = 0 - for trade in trades: - curdayprofit += trade.close_profit * trade.stake_amount - profit_days[date.fromordinal(today-day)] = format(curdayprofit, '.8f') + # need to query between day+1 and day-1 + nextdate = date.fromordinal(today-day+1) + prevdate = date.fromordinal(today-day-1) + trades = Trade.query.filter(between(Trade.close_date, prevdate, nextdate)).all() + curdayprofit = sum(trade.close_profit * trade.stake_amount for trade in trades) + profit_days[date.fromordinal(today-day)] = format(curdayprofit, '.8f') - stats = [] - for key, value in profit_days.items(): - stats.append([key, str(value) + ' BTC']) + stats = [[key, str(value) + ' BTC'] for key, value in profit_days.items()] stats = tabulate(stats, headers=['Day', 'Profit'], tablefmt='simple') message = 'Daily Profit over the last {} days:\n
{}'.format(timescale, stats)
send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
-
+
+
@authorized_only
def _profit(bot: Bot, update: Update) -> None:
"""
@@ -391,10 +388,7 @@ def _forcesell(bot: Bot, update: Update) -> None:
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
- # Get current rate
- current_rate = exchange.get_ticker(trade.pair)['bid']
- from freqtrade.main import execute_sell
- execute_sell(trade, current_rate)
+ _exec_forcesell(trade)
return
# Query for trade
@@ -406,10 +400,8 @@ def _forcesell(bot: Bot, update: Update) -> None:
send_msg('Invalid argument. See `/help` to view usage')
logger.warning('/forcesell: Invalid argument received')
return
- # Get current rate
- current_rate = exchange.get_ticker(trade.pair)['bid']
- from freqtrade.main import execute_sell
- execute_sell(trade, current_rate)
+
+ _exec_forcesell(trade)
@authorized_only
@@ -504,11 +496,11 @@ def _version(bot: Bot, update: Update) -> None:
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
-def shorten_date(date):
+def shorten_date(_date):
"""
Trim the date so it fits on small screens
"""
- new_date = re.sub('seconds?', 'sec', date)
+ new_date = re.sub('seconds?', 'sec', _date)
new_date = re.sub('minutes?', 'min', new_date)
new_date = re.sub('hours?', 'h', new_date)
new_date = re.sub('days?', 'd', new_date)
@@ -516,6 +508,28 @@ def shorten_date(date):
return new_date
+def _exec_forcesell(trade: Trade) -> None:
+ # Check if there is there is an open order
+ if trade.open_order_id:
+ order = exchange.get_order(trade.open_order_id)
+
+ # Cancel open LIMIT_BUY orders and close trade
+ if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
+ exchange.cancel_order(trade.open_order_id)
+ trade.close(order.get('rate') or trade.open_rate)
+ # TODO: sell amount which has been bought already
+ return
+
+ # Ignore trades with an attached LIMIT_SELL order
+ if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
+ return
+
+ # Get current rate and execute sell
+ current_rate = exchange.get_ticker(trade.pair)['bid']
+ from freqtrade.main import execute_sell
+ execute_sell(trade, current_rate)
+
+
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message
@@ -529,7 +543,7 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
bot = bot or _UPDATER.bot
- keyboard = [['/daily', '/profit', '/balance' ],
+ keyboard = [['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'],
['/count', '/start', '/stop', '/help']]
diff --git a/freqtrade/tests/test_rpc_telegram.py b/freqtrade/tests/test_rpc_telegram.py
index 58a9fcaa9..901450f14 100644
--- a/freqtrade/tests/test_rpc_telegram.py
+++ b/freqtrade/tests/test_rpc_telegram.py
@@ -14,7 +14,8 @@ from freqtrade.misc import update_state, State, get_state
from freqtrade.persistence import Trade
from freqtrade.rpc import telegram
from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \
- _profit, _forcesell, _performance, _daily, _count, _start, _stop, _balance, _version, _help
+ _profit, _forcesell, _performance, _daily, _count, _start, _stop, _balance, _version, _help, \
+ _exec_forcesell
def test_is_enabled(default_conf, mocker):
@@ -220,6 +221,30 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
assert '0.07256061 (profit: ~-0.64%)' in rpc_mock.call_args_list[-1][0][0]
+def test_exec_forcesell_open_orders(default_conf, ticker, mocker):
+ mocker.patch.dict('freqtrade.main._CONF', default_conf)
+ cancel_order_mock = MagicMock()
+ mocker.patch.multiple('freqtrade.main.exchange',
+ get_ticker=ticker,
+ get_order=MagicMock(return_value={
+ 'closed': None,
+ 'type': 'LIMIT_BUY',
+ }),
+ cancel_order=cancel_order_mock)
+ trade = Trade(
+ pair='BTC_ETH',
+ open_rate=1,
+ exchange='BITTREX',
+ open_order_id='123456789',
+ amount=1,
+ fee=0.0,
+ )
+ _exec_forcesell(trade)
+
+ assert cancel_order_mock.call_count == 1
+ assert trade.is_open is False
+
+
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)