Merge pull request #192 from gcarq/feature/forcesell-handle-open-orders
/forcesell: handle trades with open orders
This commit is contained in:
commit
5efc417690
@ -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).
|
||||
|
@ -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,13 +218,12 @@ 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):
|
||||
@ -231,23 +231,20 @@ def _daily(bot: Bot, update: Update) -> None:
|
||||
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 = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.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']]
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user