Added order timeout handling

This commit is contained in:
Stephen Dade 2018-01-03 14:30:10 +11:00
parent 1ce4613aad
commit b4d6250d55
6 changed files with 213 additions and 2 deletions

View File

@ -160,6 +160,7 @@ optional arguments:
"tradesv3.dry_run.sqlite" instead of memory DB. Work "tradesv3.dry_run.sqlite" instead of memory DB. Work
only if dry_run is enabled. only if dry_run is enabled.
``` ```
More details on: More details on:
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands) - [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [How to use Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands) - [How to use Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
@ -196,3 +197,4 @@ To run this bot we recommend you a cloud instance with a minimum of:
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) - [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) - [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
- [Docker](https://www.docker.com/products/docker) (Recommended) - [Docker](https://www.docker.com/products/docker) (Recommended)

View File

@ -11,6 +11,7 @@
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.10, "stoploss": -0.10,
"opentradetimeout": 600,
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },

View File

@ -5,7 +5,7 @@ import logging
import sys import sys
import time import time
import traceback import traceback
from datetime import datetime from datetime import datetime, timedelta
from typing import Dict, Optional, List from typing import Dict, Optional, List
import requests import requests
@ -98,6 +98,10 @@ 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 'opentradetimeout' in _CONF and trade.open_order_id:
# Check and handle any timed out trades
check_handle_timedout(trade)
Trade.session.flush() Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error: except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
logger.warning( logger.warning(
@ -115,6 +119,50 @@ def _process(nb_assets: Optional[int] = 0) -> bool:
return state_changed return state_changed
def check_handle_timedout(trade: Trade) -> bool:
"""
Check if a trade is timed out and cancel if neccessary
:param trade: Trade instance
:return: True if the trade is timed out, false otherwise
"""
timeoutthreashold = datetime.utcnow() - timedelta(minutes=_CONF['opentradetimeout'])
order = exchange.get_order(trade.open_order_id)
if trade.open_date < timeoutthreashold:
# Buy timeout - cancel order
exchange.cancel_order(trade.open_order_id)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
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)
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
return False
else:
return False
def execute_sell(trade: Trade, limit: float) -> None: def execute_sell(trade: Trade, limit: float) -> None:
""" """
Executes a limit sell for the given trade and limit Executes a limit sell for the given trade and limit

View File

@ -218,6 +218,7 @@ CONF_SCHEMA = {
'minProperties': 1 'minProperties': 1
}, },
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'opentradetimeout': {'type': 'integer', 'minimum': 0},
'bid_strategy': { 'bid_strategy': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {

View File

@ -25,6 +25,7 @@ def default_conf():
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.10, "stoploss": -0.10,
"opentradetimeout": 600,
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },

View File

@ -5,13 +5,14 @@ from unittest.mock import MagicMock
import pytest import pytest
import requests import requests
import logging import logging
from datetime import timedelta, datetime
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade import DependencyException, OperationalException from freqtrade import DependencyException, OperationalException
from freqtrade.analyze import SignalType from freqtrade.analyze import SignalType
from freqtrade.exchange import Exchanges from freqtrade.exchange import Exchanges
from freqtrade.main import create_trade, handle_trade, init, \ from freqtrade.main import create_trade, handle_trade, init, \
get_target_bid, _process, execute_sell get_target_bid, _process, execute_sell, check_handle_timedout
from freqtrade.misc import get_state, State from freqtrade.misc import get_state, State
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -318,6 +319,163 @@ 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):
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={
'closed': None,
'type': 'LIMIT_BUY',
'remaining': 1.0,
'amount': 1.0,
}),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
tradeBuy = Trade(
pair='BTC_ETH',
open_rate=1,
exchange='BITTREX',
open_order_id='123456789',
amount=1,
fee=0.0,
stake_amount=1,
open_date=datetime.utcnow(),
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
)
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
ret = check_handle_timedout(tradeBuy)
assert ret is True
assert cancel_order_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all()
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
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)
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={
'closed': None,
'type': 'LIMIT_BUY',
'remaining': 0.5,
'amount': 1.0,
}),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
tradeBuy = Trade(
pair='BTC_ETH',
open_rate=1,
exchange='BITTREX',
open_order_id='123456789',
amount=1,
fee=0.0,
stake_amount=1,
open_date=datetime.utcnow(),
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
)
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
# note this is for a partially-complete buy order
ret = check_handle_timedout(tradeBuy)
assert ret is True
assert cancel_order_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all()
assert len(trades) == 1
assert trades[0].amount == 0.5
assert trades[0].stake_amount == 0.5
# 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):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}}) mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 20 assert get_target_bid({'ask': 20, 'last': 10}) == 20