Merge pull request #1358 from mishaker/time_in_force
Order Time In Force
This commit is contained in:
commit
5d253f352c
@ -39,6 +39,10 @@
|
|||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": "false"
|
"stoploss_on_exchange": "false"
|
||||||
},
|
},
|
||||||
|
"order_time_in_force": {
|
||||||
|
"buy": "gtc",
|
||||||
|
"sell": "gtc",
|
||||||
|
},
|
||||||
"pairlist": {
|
"pairlist": {
|
||||||
"method": "VolumePairList",
|
"method": "VolumePairList",
|
||||||
"config": {
|
"config": {
|
||||||
|
@ -40,6 +40,7 @@ The table below will list all configuration parameters.
|
|||||||
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types).
|
| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types).
|
||||||
|
| `order_time_in_force` | None | No | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force).
|
||||||
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
||||||
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
||||||
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
||||||
@ -161,6 +162,25 @@ The below is the default which is used if this is not configured in either Strat
|
|||||||
**NOTE**: Not all exchanges support "market" orders.
|
**NOTE**: Not all exchanges support "market" orders.
|
||||||
The following message will be shown if your exchange does not support market orders: `"Exchange <yourexchange> does not support market orders."`
|
The following message will be shown if your exchange does not support market orders: `"Exchange <yourexchange> does not support market orders."`
|
||||||
|
|
||||||
|
### Understand order_time_in_force
|
||||||
|
Order time in force defines the policy by which the order is executed on the exchange. Three commonly used time in force are:<br/>
|
||||||
|
**GTC (Goog Till Canceled):**
|
||||||
|
This is most of the time the default time in force. It means the order will remain on exchange till it is canceled by user. It can be fully or partially fulfilled. If partially fulfilled, the remaining will stay on the exchange till cancelled.<br/>
|
||||||
|
**FOK (Full Or Kill):**
|
||||||
|
It means if the order is not executed immediately AND fully then it is canceled by the exchange.<br/>
|
||||||
|
**IOC (Immediate Or Canceled):**
|
||||||
|
It is the same as FOK (above) except it can be partially fulfilled. The remaining part is automatically cancelled by the exchange.
|
||||||
|
<br/>
|
||||||
|
`order_time_in_force` contains a dict buy and sell time in force policy. This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations.<br/>
|
||||||
|
possible values are: `gtc` (default), `fok` or `ioc`.<br/>
|
||||||
|
``` python
|
||||||
|
"order_time_in_force": {
|
||||||
|
"buy": "gtc",
|
||||||
|
"sell": "gtc"
|
||||||
|
},
|
||||||
|
```
|
||||||
|
**NOTE**: This is an ongoing work. For now it is supported only for binance and only for buy orders. Please don't change the default value unless you know what you are doing.<br/>
|
||||||
|
|
||||||
### What values for exchange.name?
|
### What values for exchange.name?
|
||||||
|
|
||||||
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
||||||
|
@ -13,8 +13,10 @@ DEFAULT_HYPEROPT = 'DefaultHyperOpts'
|
|||||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||||
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
||||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||||
|
REQUIRED_ORDERTIF = ['buy', 'sell']
|
||||||
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
|
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
|
||||||
|
|
||||||
TICKER_INTERVAL_MINUTES = {
|
TICKER_INTERVAL_MINUTES = {
|
||||||
@ -114,6 +116,14 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||||
},
|
},
|
||||||
|
'order_time_in_force': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'buy': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES},
|
||||||
|
'sell': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES}
|
||||||
|
},
|
||||||
|
'required': ['buy', 'sell']
|
||||||
|
},
|
||||||
'exchange': {'$ref': '#/definitions/exchange'},
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
'edge': {'$ref': '#/definitions/edge'},
|
'edge': {'$ref': '#/definitions/edge'},
|
||||||
'experimental': {
|
'experimental': {
|
||||||
|
@ -103,6 +103,7 @@ class Exchange(object):
|
|||||||
# Check if all pairs are available
|
# Check if all pairs are available
|
||||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
self.validate_ordertypes(config.get('order_types', {}))
|
self.validate_ordertypes(config.get('order_types', {}))
|
||||||
|
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||||
if config.get('ticker_interval'):
|
if config.get('ticker_interval'):
|
||||||
# Check if timeframe is available
|
# Check if timeframe is available
|
||||||
self.validate_timeframes(config['ticker_interval'])
|
self.validate_timeframes(config['ticker_interval'])
|
||||||
@ -240,6 +241,15 @@ class Exchange(object):
|
|||||||
'On exchange stoploss is not supported for %s.' % self.name
|
'On exchange stoploss is not supported for %s.' % self.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
|
||||||
|
"""
|
||||||
|
Checks if order time in force configured in strategy/config are supported
|
||||||
|
"""
|
||||||
|
if any(v != 'gtc' for k, v in order_time_in_force.items()):
|
||||||
|
if self.name is not 'Binance':
|
||||||
|
raise OperationalException(
|
||||||
|
f'Time in force policies are not supporetd for {self.name} yet.')
|
||||||
|
|
||||||
def exchange_has(self, endpoint: str) -> bool:
|
def exchange_has(self, endpoint: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if exchange implements a specific API endpoint.
|
Checks if exchange implements a specific API endpoint.
|
||||||
@ -271,7 +281,8 @@ class Exchange(object):
|
|||||||
price = ceil(big_price) / pow(10, symbol_prec)
|
price = ceil(big_price) / pow(10, symbol_prec)
|
||||||
return price
|
return price
|
||||||
|
|
||||||
def buy(self, pair: str, ordertype: str, amount: float, rate: float) -> Dict:
|
def buy(self, pair: str, ordertype: str, amount: float,
|
||||||
|
rate: float, time_in_force) -> Dict:
|
||||||
if self._conf['dry_run']:
|
if self._conf['dry_run']:
|
||||||
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||||
self._dry_run_open_orders[order_id] = {
|
self._dry_run_open_orders[order_id] = {
|
||||||
@ -292,7 +303,12 @@ class Exchange(object):
|
|||||||
amount = self.symbol_amount_prec(pair, amount)
|
amount = self.symbol_amount_prec(pair, amount)
|
||||||
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
|
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
|
||||||
|
|
||||||
|
if time_in_force == 'gtc':
|
||||||
return self._api.create_order(pair, ordertype, 'buy', amount, rate)
|
return self._api.create_order(pair, ordertype, 'buy', amount, rate)
|
||||||
|
else:
|
||||||
|
return self._api.create_order(pair, ordertype, 'buy',
|
||||||
|
amount, rate, {'timeInForce': time_in_force})
|
||||||
|
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f'Insufficient funds to create limit buy order on market {pair}.'
|
f'Insufficient funds to create limit buy order on market {pair}.'
|
||||||
@ -309,7 +325,8 @@ class Exchange(object):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
def sell(self, pair: str, ordertype: str, amount: float, rate: float) -> Dict:
|
def sell(self, pair: str, ordertype: str, amount: float,
|
||||||
|
rate: float, time_in_force='gtc') -> Dict:
|
||||||
if self._conf['dry_run']:
|
if self._conf['dry_run']:
|
||||||
order_id = f'dry_run_sell_{randint(0, 10**6)}'
|
order_id = f'dry_run_sell_{randint(0, 10**6)}'
|
||||||
self._dry_run_open_orders[order_id] = {
|
self._dry_run_open_orders[order_id] = {
|
||||||
@ -329,7 +346,12 @@ class Exchange(object):
|
|||||||
amount = self.symbol_amount_prec(pair, amount)
|
amount = self.symbol_amount_prec(pair, amount)
|
||||||
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
|
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
|
||||||
|
|
||||||
|
if time_in_force == 'gtc':
|
||||||
return self._api.create_order(pair, ordertype, 'sell', amount, rate)
|
return self._api.create_order(pair, ordertype, 'sell', amount, rate)
|
||||||
|
else:
|
||||||
|
return self._api.create_order(pair, ordertype, 'sell',
|
||||||
|
amount, rate, {'timeInForce': time_in_force})
|
||||||
|
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f'Insufficient funds to create limit sell order on market {pair}.'
|
f'Insufficient funds to create limit sell order on market {pair}.'
|
||||||
|
@ -367,14 +367,15 @@ class FreqtradeBot(object):
|
|||||||
pair_url = self.exchange.get_pair_detail_url(pair)
|
pair_url = self.exchange.get_pair_detail_url(pair)
|
||||||
stake_currency = self.config['stake_currency']
|
stake_currency = self.config['stake_currency']
|
||||||
fiat_currency = self.config.get('fiat_display_currency', None)
|
fiat_currency = self.config.get('fiat_display_currency', None)
|
||||||
|
time_in_force = self.strategy.order_time_in_force['buy']
|
||||||
|
|
||||||
if price:
|
if price:
|
||||||
buy_limit = price
|
buy_limit_requested = price
|
||||||
else:
|
else:
|
||||||
# Calculate amount
|
# Calculate amount
|
||||||
buy_limit = self.get_target_bid(pair, self.exchange.get_ticker(pair))
|
buy_limit_requested = self.get_target_bid(pair, self.exchange.get_ticker(pair))
|
||||||
|
|
||||||
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
|
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested)
|
||||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'Can\'t open a new trade for {pair_s}: stake amount'
|
f'Can\'t open a new trade for {pair_s}: stake amount'
|
||||||
@ -382,17 +383,54 @@ class FreqtradeBot(object):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
amount = stake_amount / buy_limit
|
amount = stake_amount / buy_limit_requested
|
||||||
|
|
||||||
order_id = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'],
|
order = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'],
|
||||||
amount=amount, rate=buy_limit)['id']
|
amount=amount, rate=buy_limit_requested,
|
||||||
|
time_in_force=time_in_force)
|
||||||
|
order_id = order['id']
|
||||||
|
order_status = order.get('status', None)
|
||||||
|
|
||||||
|
# we assume the order is executed at the price requested
|
||||||
|
buy_limit_filled_price = buy_limit_requested
|
||||||
|
|
||||||
|
if order_status == 'expired' or order_status == 'rejected':
|
||||||
|
order_type = self.strategy.order_types['buy']
|
||||||
|
order_tif = self.strategy.order_time_in_force['buy']
|
||||||
|
|
||||||
|
# return false if the order is not filled
|
||||||
|
if float(order['filled']) == 0:
|
||||||
|
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
|
||||||
|
' zero amount is fulfilled.',
|
||||||
|
order_tif, order_type, pair_s, order_status, self.exchange.name)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# the order is partially fulfilled
|
||||||
|
# in case of IOC orders we can check immediately
|
||||||
|
# if the order is fulfilled fully or partially
|
||||||
|
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
|
||||||
|
' %s amount fulfilled out of %s (%s remaining which is canceled).',
|
||||||
|
order_tif, order_type, pair_s, order_status, self.exchange.name,
|
||||||
|
order['filled'], order['amount'], order['remaining']
|
||||||
|
)
|
||||||
|
stake_amount = order['cost']
|
||||||
|
amount = order['amount']
|
||||||
|
buy_limit_filled_price = order['price']
|
||||||
|
order_id = None
|
||||||
|
|
||||||
|
# in case of FOK the order may be filled immediately and fully
|
||||||
|
elif order_status == 'closed':
|
||||||
|
stake_amount = order['cost']
|
||||||
|
amount = order['amount']
|
||||||
|
buy_limit_filled_price = order['price']
|
||||||
|
order_id = None
|
||||||
|
|
||||||
self.rpc.send_msg({
|
self.rpc.send_msg({
|
||||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': pair_s,
|
'pair': pair_s,
|
||||||
'market_url': pair_url,
|
'market_url': pair_url,
|
||||||
'limit': buy_limit,
|
'limit': buy_limit_filled_price,
|
||||||
'stake_amount': stake_amount,
|
'stake_amount': stake_amount,
|
||||||
'stake_currency': stake_currency,
|
'stake_currency': stake_currency,
|
||||||
'fiat_currency': fiat_currency
|
'fiat_currency': fiat_currency
|
||||||
@ -406,8 +444,8 @@ class FreqtradeBot(object):
|
|||||||
amount=amount,
|
amount=amount,
|
||||||
fee_open=fee,
|
fee_open=fee,
|
||||||
fee_close=fee,
|
fee_close=fee,
|
||||||
open_rate=buy_limit,
|
open_rate=buy_limit_filled_price,
|
||||||
open_rate_requested=buy_limit,
|
open_rate_requested=buy_limit_requested,
|
||||||
open_date=datetime.utcnow(),
|
open_date=datetime.utcnow(),
|
||||||
exchange=self.exchange.id,
|
exchange=self.exchange.id,
|
||||||
open_order_id=order_id,
|
open_order_id=order_id,
|
||||||
@ -751,7 +789,10 @@ class FreqtradeBot(object):
|
|||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order_id = self.exchange.sell(pair=str(trade.pair),
|
order_id = self.exchange.sell(pair=str(trade.pair),
|
||||||
ordertype=self.strategy.order_types[sell_type],
|
ordertype=self.strategy.order_types[sell_type],
|
||||||
amount=trade.amount, rate=limit)['id']
|
amount=trade.amount, rate=limit,
|
||||||
|
time_in_force=self.strategy.order_time_in_force['sell']
|
||||||
|
)['id']
|
||||||
|
|
||||||
trade.open_order_id = order_id
|
trade.open_order_id = order_id
|
||||||
trade.close_rate_requested = limit
|
trade.close_rate_requested = limit
|
||||||
trade.sell_reason = sell_reason.value
|
trade.sell_reason = sell_reason.value
|
||||||
|
@ -83,10 +83,23 @@ class StrategyResolver(IResolver):
|
|||||||
else:
|
else:
|
||||||
config['order_types'] = self.strategy.order_types
|
config['order_types'] = self.strategy.order_types
|
||||||
|
|
||||||
|
if 'order_time_in_force' in config:
|
||||||
|
self.strategy.order_time_in_force = config['order_time_in_force']
|
||||||
|
logger.info(
|
||||||
|
"Override strategy 'order_time_in_force' with value in config file: %s.",
|
||||||
|
config['order_time_in_force']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
config['order_time_in_force'] = self.strategy.order_time_in_force
|
||||||
|
|
||||||
if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES):
|
if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES):
|
||||||
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
||||||
f"Order-types mapping is incomplete.")
|
f"Order-types mapping is incomplete.")
|
||||||
|
|
||||||
|
if not all(k in self.strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF):
|
||||||
|
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
||||||
|
f"Order-time-in-force mapping is incomplete.")
|
||||||
|
|
||||||
# Sort and apply type conversions
|
# Sort and apply type conversions
|
||||||
self.strategy.minimal_roi = OrderedDict(sorted(
|
self.strategy.minimal_roi = OrderedDict(sorted(
|
||||||
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
|
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
|
||||||
|
@ -36,6 +36,12 @@ class DefaultStrategy(IStrategy):
|
|||||||
'stoploss_on_exchange': False
|
'stoploss_on_exchange': False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Optional time in force for orders
|
||||||
|
order_time_in_force = {
|
||||||
|
'buy': 'gtc',
|
||||||
|
'sell': 'gtc',
|
||||||
|
}
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
@ -79,6 +79,12 @@ class IStrategy(ABC):
|
|||||||
'stoploss_on_exchange': False
|
'stoploss_on_exchange': False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Optional time in force
|
||||||
|
order_time_in_force: Dict = {
|
||||||
|
'buy': 'gtc',
|
||||||
|
'sell': 'gtc',
|
||||||
|
}
|
||||||
|
|
||||||
# run "populate_indicators" only for new candle
|
# run "populate_indicators" only for new candle
|
||||||
process_only_new_candles: bool = False
|
process_only_new_candles: bool = False
|
||||||
|
|
||||||
|
@ -427,7 +427,8 @@ def test_buy_dry_run(default_conf, mocker):
|
|||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
order = exchange.buy(pair='ETH/BTC', ordertype='limit', amount=1, rate=200)
|
order = exchange.buy(pair='ETH/BTC', ordertype='limit',
|
||||||
|
amount=1, rate=200, time_in_force='gtc')
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'dry_run_buy_' in order['id']
|
assert 'dry_run_buy_' in order['id']
|
||||||
|
|
||||||
@ -436,6 +437,7 @@ def test_buy_prod(default_conf, mocker):
|
|||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
order_type = 'market'
|
order_type = 'market'
|
||||||
|
time_in_force = 'gtc'
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'info': {
|
'info': {
|
||||||
@ -447,7 +449,9 @@ def test_buy_prod(default_conf, mocker):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
order = exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert order['id'] == order_id
|
assert order['id'] == order_id
|
||||||
@ -459,7 +463,12 @@ def test_buy_prod(default_conf, mocker):
|
|||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
order_type = 'limit'
|
order_type = 'limit'
|
||||||
order = exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
order = exchange.buy(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
ordertype=order_type,
|
||||||
|
amount=1,
|
||||||
|
rate=200,
|
||||||
|
time_in_force=time_in_force)
|
||||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||||
assert api_mock.create_order.call_args[0][1] == order_type
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
assert api_mock.create_order.call_args[0][2] == 'buy'
|
assert api_mock.create_order.call_args[0][2] == 'buy'
|
||||||
@ -470,22 +479,56 @@ def test_buy_prod(default_conf, mocker):
|
|||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
with pytest.raises(TemporaryError):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError)
|
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError)
|
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
|
|
||||||
|
def test_buy_considers_time_in_force(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
order_type = 'market'
|
||||||
|
time_in_force = 'ioc'
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
|
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
|
assert api_mock.create_order.call_args[0][2] == 'buy'
|
||||||
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
|
assert api_mock.create_order.call_args[0][4] is None
|
||||||
|
assert api_mock.create_order.call_args[0][5] == {'timeInForce': 'ioc'}
|
||||||
|
|
||||||
|
|
||||||
def test_sell_dry_run(default_conf, mocker):
|
def test_sell_dry_run(default_conf, mocker):
|
||||||
|
@ -221,6 +221,41 @@ def test_strategy_override_order_types(caplog):
|
|||||||
StrategyResolver(config)
|
StrategyResolver(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_order_tif(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
order_time_in_force = {
|
||||||
|
'buy': 'fok',
|
||||||
|
'sell': 'gtc',
|
||||||
|
}
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'order_time_in_force': order_time_in_force
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert resolver.strategy.order_time_in_force
|
||||||
|
for method in ['buy', 'sell']:
|
||||||
|
assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method]
|
||||||
|
|
||||||
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
|
logging.INFO,
|
||||||
|
"Override strategy 'order_time_in_force' with value in config file:"
|
||||||
|
" {'buy': 'fok', 'sell': 'gtc'}."
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'order_time_in_force': {'buy': 'fok'}
|
||||||
|
}
|
||||||
|
# Raise error for invalid configuration
|
||||||
|
with pytest.raises(ImportError,
|
||||||
|
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
||||||
|
r"Order-time-in-force mapping is incomplete."):
|
||||||
|
StrategyResolver(config)
|
||||||
|
|
||||||
|
|
||||||
def test_deprecate_populate_indicators(result):
|
def test_deprecate_populate_indicators(result):
|
||||||
default_location = path.join(path.dirname(path.realpath(__file__)))
|
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||||
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
||||||
|
@ -863,6 +863,13 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non
|
|||||||
assert call_args['rate'] == bid
|
assert call_args['rate'] == bid
|
||||||
assert call_args['amount'] == stake_amount / bid
|
assert call_args['amount'] == stake_amount / bid
|
||||||
|
|
||||||
|
# Should create an open trade with an open order id
|
||||||
|
# As the order is not fulfilled yet
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
assert trade.is_open is True
|
||||||
|
assert trade.open_order_id == limit_buy_order['id']
|
||||||
|
|
||||||
# Test calling with price
|
# Test calling with price
|
||||||
fix_price = 0.06
|
fix_price = 0.06
|
||||||
assert freqtrade.execute_buy(pair, stake_amount, fix_price)
|
assert freqtrade.execute_buy(pair, stake_amount, fix_price)
|
||||||
@ -875,6 +882,43 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non
|
|||||||
assert call_args['rate'] == fix_price
|
assert call_args['rate'] == fix_price
|
||||||
assert call_args['amount'] == stake_amount / fix_price
|
assert call_args['amount'] == stake_amount / fix_price
|
||||||
|
|
||||||
|
# In case of closed order
|
||||||
|
limit_buy_order['status'] = 'closed'
|
||||||
|
limit_buy_order['price'] = 10
|
||||||
|
limit_buy_order['cost'] = 100
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
|
||||||
|
assert freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
trade = Trade.query.all()[2]
|
||||||
|
assert trade
|
||||||
|
assert trade.open_order_id is None
|
||||||
|
assert trade.open_rate == 10
|
||||||
|
assert trade.stake_amount == 100
|
||||||
|
|
||||||
|
# In case of rejected or expired order and partially filled
|
||||||
|
limit_buy_order['status'] = 'expired'
|
||||||
|
limit_buy_order['amount'] = 90.99181073
|
||||||
|
limit_buy_order['filled'] = 80.99181073
|
||||||
|
limit_buy_order['remaining'] = 10.00
|
||||||
|
limit_buy_order['price'] = 0.5
|
||||||
|
limit_buy_order['cost'] = 40.495905365
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
|
||||||
|
assert freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
trade = Trade.query.all()[3]
|
||||||
|
assert trade
|
||||||
|
assert trade.open_order_id is None
|
||||||
|
assert trade.open_rate == 0.5
|
||||||
|
assert trade.stake_amount == 40.495905365
|
||||||
|
|
||||||
|
# In case of the order is rejected and not filled at all
|
||||||
|
limit_buy_order['status'] = 'rejected'
|
||||||
|
limit_buy_order['amount'] = 90.99181073
|
||||||
|
limit_buy_order['filled'] = 0.0
|
||||||
|
limit_buy_order['remaining'] = 90.99181073
|
||||||
|
limit_buy_order['price'] = 0.5
|
||||||
|
limit_buy_order['cost'] = 0.0
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
|
||||||
|
assert not freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
|
||||||
|
|
||||||
def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None:
|
def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
@ -56,6 +56,12 @@ class TestStrategy(IStrategy):
|
|||||||
'stoploss_on_exchange': False
|
'stoploss_on_exchange': False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Optional order time in force
|
||||||
|
order_time_in_force = {
|
||||||
|
'buy': 'gtc',
|
||||||
|
'sell': 'gtc'
|
||||||
|
}
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
Loading…
Reference in New Issue
Block a user