Merge pull request #192 from gcarq/feature/forcesell-handle-open-orders

/forcesell: handle trades with open orders
This commit is contained in:
Janne Sinivirta 2017-12-17 07:41:51 +02:00 committed by GitHub
commit 5efc417690
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 87 additions and 41 deletions

View File

@ -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).

View File

@ -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 = '<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']]

View File

@ -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)