Merge pull request #306 from stephendade/timeoutfix

Unfilled order timeouts - now using timestamps from exchange
This commit is contained in:
Janne Sinivirta 2018-01-05 18:04:27 +02:00 committed by GitHub
commit 833c7f21af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 138 additions and 153 deletions

View File

@ -5,7 +5,8 @@ import logging
import sys import sys
import time import time
import traceback import traceback
from datetime import datetime, timedelta import arrow
from datetime import datetime
from typing import Dict, Optional, List from typing import Dict, Optional, List
import requests import requests
@ -98,9 +99,9 @@ def _process(nb_assets: Optional[int] = 0) -> bool:
# Check if we can sell our current pair # Check if we can sell our current pair
state_changed = handle_trade(trade) or state_changed state_changed = handle_trade(trade) or state_changed
if 'unfilledtimeout' in _CONF and trade.open_order_id: if 'unfilledtimeout' in _CONF:
# Check and handle any timed out trades # Check and handle any timed out open orders
check_handle_timedout(trade) check_handle_timedout(_CONF['unfilledtimeout'])
Trade.session.flush() Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error: except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
@ -119,48 +120,48 @@ def _process(nb_assets: Optional[int] = 0) -> bool:
return state_changed return state_changed
def check_handle_timedout(trade: Trade) -> bool: def check_handle_timedout(timeoutvalue: int) -> None:
""" """
Check if a trade is timed out and cancel if neccessary Check if any orders are timed out and cancel if neccessary
:param trade: Trade instance :param timeoutvalue: Number of minutes until order is considered timed out
:return: True if the trade is timed out, false otherwise :return: None
""" """
timeoutthreashold = datetime.utcnow() - timedelta(minutes=_CONF['unfilledtimeout']) timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
order = exchange.get_order(trade.open_order_id)
if trade.open_date < timeoutthreashold: for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
# Buy timeout - cancel order order = exchange.get_order(trade.open_order_id)
exchange.cancel_order(trade.open_order_id) ordertime = arrow.get(order['opened'])
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
Trade.session.delete(trade) # Buy timeout - cancel order
Trade.session.flush()
logger.info('Buy order timeout for %s.', trade)
else:
# if trade is partially complete, edit the stake details for the trade
# and close the order
trade.amount = order['amount'] - order['remaining']
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
return True
elif trade.close_date is not None and trade.close_date < timeoutthreashold:
# Sell timeout - cancel order and update trade
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id) exchange.cancel_order(trade.open_order_id)
trade.close_rate = None if order['remaining'] == order['amount']:
trade.close_profit = None # if trade is not partially completed, just delete the trade
trade.close_date = None Trade.session.delete(trade)
trade.is_open = True Trade.session.flush()
trade.open_order_id = None logger.info('Buy order timeout for %s.', trade)
logger.info('Sell order timeout for %s.', trade) else:
return True # if trade is partially complete, edit the stake details for the trade
else: # and close the order
# TODO: figure out how to handle partially complete sell orders trade.amount = order['amount'] - order['remaining']
return False trade.stake_amount = trade.amount * trade.open_rate
else: trade.open_order_id = None
return False logger.info('Partial buy order timeout for %s.', trade)
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
# Sell timeout - cancel order and update trade
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
logger.info('Sell order timeout for %s.', trade)
return True
else:
# TODO: figure out how to handle partially complete sell orders
pass
def execute_sell(trade: Trade, limit: float) -> None: def execute_sell(trade: Trade, limit: float) -> None:

View File

@ -3,6 +3,7 @@ from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
import arrow
from jsonschema import validate from jsonschema import validate
from telegram import Message, Chat, Update from telegram import Message, Chat, Update
@ -123,11 +124,50 @@ def limit_buy_order():
'id': 'mocked_limit_buy', 'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY', 'type': 'LIMIT_BUY',
'pair': 'mocked', 'pair': 'mocked',
'opened': datetime.utcnow(), 'opened': str(arrow.utcnow().datetime),
'rate': 0.00001099, 'rate': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
'remaining': 0.0, 'remaining': 0.0,
'closed': datetime.utcnow(), 'closed': str(arrow.utcnow().datetime),
}
@pytest.fixture
def limit_buy_order_old():
return {
'id': 'mocked_limit_buy_old',
'type': 'LIMIT_BUY',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'amount': 90.99181073,
'remaining': 90.99181073,
}
@pytest.fixture
def limit_sell_order_old():
return {
'id': 'mocked_limit_sell_old',
'type': 'LIMIT_SELL',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'amount': 90.99181073,
'remaining': 90.99181073,
}
@pytest.fixture
def limit_buy_order_old_partial():
return {
'id': 'mocked_limit_buy_old_partial',
'type': 'LIMIT_BUY',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'amount': 90.99181073,
'remaining': 67.99181073,
} }
@ -137,11 +177,11 @@ def limit_sell_order():
'id': 'mocked_limit_sell', 'id': 'mocked_limit_sell',
'type': 'LIMIT_SELL', 'type': 'LIMIT_SELL',
'pair': 'mocked', 'pair': 'mocked',
'opened': datetime.utcnow(), 'opened': str(arrow.utcnow().datetime),
'rate': 0.00001173, 'rate': 0.00001173,
'amount': 90.99181073, 'amount': 90.99181073,
'remaining': 0.0, 'remaining': 0.0,
'closed': datetime.utcnow(), 'closed': str(arrow.utcnow().datetime),
} }

View File

@ -5,7 +5,7 @@ from unittest.mock import MagicMock
import pytest import pytest
import requests import requests
import logging import logging
from datetime import timedelta, datetime import arrow
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade import DependencyException, OperationalException from freqtrade import DependencyException, OperationalException
@ -17,7 +17,7 @@ from freqtrade.misc import get_state, State
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
def test_process_trade_creation(default_conf, ticker, health, mocker): def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
@ -25,7 +25,8 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health, get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy')) buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
@ -319,161 +320,104 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
handle_trade(trade) handle_trade(trade)
def test_check_handle_timedout(default_conf, ticker, health, mocker): def test_check_handle_timedout_buy(default_conf, ticker, health, limit_buy_order_old, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_order=MagicMock(return_value={ get_order=MagicMock(return_value=limit_buy_order_old),
'closed': None,
'type': 'LIMIT_BUY',
'remaining': 1.0,
'amount': 1.0,
}),
cancel_order=cancel_order_mock) cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
tradeBuy = Trade( tradeBuy = Trade(
pair='BTC_ETH', pair='BTC_ETH',
open_rate=1, open_rate=0.00001099,
exchange='BITTREX', exchange='BITTREX',
open_order_id='123456789', open_order_id='123456789',
amount=1, amount=90.99181073,
fee=0.0, fee=0.0,
stake_amount=1, stake_amount=1,
open_date=datetime.utcnow(), open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
)
tradeSell = Trade(
pair='BTC_BCC',
open_rate=1,
exchange='BITTREX',
open_order_id='678901234',
amount=1,
fee=0.0,
stake_amount=1,
open_date=datetime.utcnow(),
close_date=datetime.utcnow(),
is_open=True is_open=True
) )
Trade.session.add(tradeBuy) Trade.session.add(tradeBuy)
Trade.session.add(tradeSell)
# check it doesn't cancel any buy trades under the time limit
ret = check_handle_timedout(tradeBuy)
assert ret is False
assert cancel_order_mock.call_count == 0
trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all()
assert len(trades) == 1
# change the trade open datetime to 601 minutes in the past
tradeBuy.open_date = datetime.utcnow() - timedelta(minutes=601)
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
ret = check_handle_timedout(tradeBuy) check_handle_timedout(600)
assert ret is True
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all() trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all()
assert len(trades) == 0 assert len(trades) == 0
# check it doesn't cancel any sell trades under the time limit
ret = check_handle_timedout(tradeSell)
assert ret is False
assert cancel_order_mock.call_count == 1
assert tradeSell.is_open is True
# change the trade close datetime to 601 minutes in the past def test_check_handle_timedout_sell(default_conf, ticker, health, limit_sell_order_old, mocker):
tradeSell.close_date = datetime.utcnow() - timedelta(minutes=601)
# check it does cancel sell orders over the time limit
ret = check_handle_timedout(tradeSell)
assert ret is True
assert cancel_order_mock.call_count == 2
assert tradeSell.is_open is True
def test_check_handle_timedout_partial(default_conf, ticker, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_order=MagicMock(return_value={ get_order=MagicMock(return_value=limit_sell_order_old),
'closed': None, cancel_order=cancel_order_mock)
'type': 'LIMIT_BUY', init(default_conf, create_engine('sqlite://'))
'remaining': 0.5,
'amount': 1.0, tradeSell = Trade(
}), pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(hours=-5).datetime,
close_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=False
)
Trade.session.add(tradeSell)
# check it does cancel sell orders over the time limit
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
assert tradeSell.is_open is True
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order=cancel_order_mock) cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
tradeBuy = Trade( tradeBuy = Trade(
pair='BTC_ETH', pair='BTC_ETH',
open_rate=1, open_rate=0.00001099,
exchange='BITTREX', exchange='BITTREX',
open_order_id='123456789', open_order_id='123456789',
amount=1, amount=90.99181073,
fee=0.0, fee=0.0,
stake_amount=1, stake_amount=1,
open_date=datetime.utcnow(), open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
)
tradeSell = Trade(
pair='BTC_BCC',
open_rate=1,
exchange='BITTREX',
open_order_id='678901234',
amount=1,
fee=0.0,
stake_amount=1,
open_date=datetime.utcnow(),
close_date=datetime.utcnow(),
is_open=True is_open=True
) )
Trade.session.add(tradeBuy) Trade.session.add(tradeBuy)
Trade.session.add(tradeSell)
# check it doesn't cancel any buy trades under the time limit
ret = check_handle_timedout(tradeBuy)
assert ret is False
assert cancel_order_mock.call_count == 0
trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all()
assert len(trades) == 1
# change the trade open datetime to 601 minutes in the past
tradeBuy.open_date = datetime.utcnow() - timedelta(minutes=601)
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
# note this is for a partially-complete buy order # note this is for a partially-complete buy order
ret = check_handle_timedout(tradeBuy) check_handle_timedout(600)
assert ret is True
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all() trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all()
assert len(trades) == 1 assert len(trades) == 1
assert trades[0].amount == 0.5 assert trades[0].amount == 23.0
assert trades[0].stake_amount == 0.5 assert trades[0].stake_amount == tradeBuy.open_rate * trades[0].amount
# check it doesn't cancel any sell trades under the time limit
ret = check_handle_timedout(tradeSell)
assert ret is False
assert cancel_order_mock.call_count == 1
assert tradeSell.is_open is True
# change the trade close datetime to 601 minutes in the past
tradeSell.close_date = datetime.utcnow() - timedelta(minutes=601)
# check it does not cancel partially-complete sell orders over the time limit
ret = check_handle_timedout(tradeSell)
assert ret is False
assert cancel_order_mock.call_count == 1
assert tradeSell.open_order_id is not None
def test_balance_fully_ask_side(mocker): def test_balance_fully_ask_side(mocker):