Merge pull request #5347 from Axel-CH/custom_order_price
Custom order price
This commit is contained in:
commit
f7087feeb1
@ -36,11 +36,12 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
|
|||||||
* Calls `check_sell_timeout()` strategy callback for open sell orders.
|
* Calls `check_sell_timeout()` strategy callback for open sell orders.
|
||||||
* Verifies existing positions and eventually places sell orders.
|
* Verifies existing positions and eventually places sell orders.
|
||||||
* Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`.
|
* Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`.
|
||||||
* Determine sell-price based on `ask_strategy` configuration setting.
|
* Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
|
||||||
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
|
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||||
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
||||||
* Verifies buy signal trying to enter new positions.
|
* Verifies buy signal trying to enter new positions.
|
||||||
* Determine buy-price based on `bid_strategy` configuration setting.
|
* Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
|
||||||
|
* Determine stake size by calling the `custom_stake_amount()` callback.
|
||||||
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
|
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
|
||||||
|
|
||||||
This loop will be repeated again and again until the bot is stopped.
|
This loop will be repeated again and again until the bot is stopped.
|
||||||
|
@ -110,6 +110,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
|
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
|
||||||
| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
|
| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
|
||||||
| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||||
|
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
||||||
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
||||||
| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> **Datatype:** Boolean
|
| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> **Datatype:** Boolean
|
||||||
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
|
@ -357,6 +357,55 @@ See [Dataframe access](#dataframe-access) for more information about dataframe u
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Custom order price rules
|
||||||
|
|
||||||
|
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||||
|
|
||||||
|
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and custom_exit_price for exits.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
||||||
|
|
||||||
|
### Custom order entry and exit price example
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def custom_entry_price(self, pair: str, current_time: datetime,
|
||||||
|
proposed_rate, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
|
timeframe=self.timeframe)
|
||||||
|
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
||||||
|
|
||||||
|
return new_entryprice
|
||||||
|
|
||||||
|
def custom_exit_price(self, pair: str, trade: Trade,
|
||||||
|
current_time: datetime, proposed_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
|
timeframe=self.timeframe)
|
||||||
|
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
||||||
|
|
||||||
|
return new_exitprice
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
|
||||||
|
|
||||||
|
!!! Example
|
||||||
|
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98.
|
||||||
|
|
||||||
|
!!! Warning "No backtesting support"
|
||||||
|
Custom entry-prices are currently not supported during backtesting.
|
||||||
|
|
||||||
## Custom order timeout rules
|
## Custom order timeout rules
|
||||||
|
|
||||||
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
||||||
|
@ -191,6 +191,9 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'required': ['price_side']
|
'required': ['price_side']
|
||||||
},
|
},
|
||||||
|
'custom_price_max_distance_ratio': {
|
||||||
|
'type': 'number', 'minimum': 0.0
|
||||||
|
},
|
||||||
'order_types': {
|
'order_types': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
@ -479,7 +479,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
buy_limit_requested = price
|
buy_limit_requested = price
|
||||||
else:
|
else:
|
||||||
# Calculate price
|
# Calculate price
|
||||||
buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy")
|
proposed_buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||||
|
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||||
|
default_retval=proposed_buy_rate)(
|
||||||
|
pair=pair, current_time=datetime.now(timezone.utc),
|
||||||
|
proposed_rate=proposed_buy_rate)
|
||||||
|
|
||||||
|
buy_limit_requested = self.get_valid_price(custom_entry_price, proposed_buy_rate)
|
||||||
|
|
||||||
if not buy_limit_requested:
|
if not buy_limit_requested:
|
||||||
raise PricingError('Could not determine buy price.')
|
raise PricingError('Could not determine buy price.')
|
||||||
@ -1076,6 +1082,17 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
and self.strategy.order_types['stoploss_on_exchange']:
|
and self.strategy.order_types['stoploss_on_exchange']:
|
||||||
limit = trade.stop_loss
|
limit = trade.stop_loss
|
||||||
|
|
||||||
|
# set custom_exit_price if available
|
||||||
|
proposed_limit_rate = limit
|
||||||
|
current_profit = trade.calc_profit_ratio(limit)
|
||||||
|
custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||||
|
default_retval=proposed_limit_rate)(
|
||||||
|
pair=trade.pair, trade=trade,
|
||||||
|
current_time=datetime.now(timezone.utc),
|
||||||
|
proposed_rate=proposed_limit_rate, current_profit=current_profit)
|
||||||
|
|
||||||
|
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
||||||
|
|
||||||
# First cancelling stoploss on exchange ...
|
# First cancelling stoploss on exchange ...
|
||||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||||
try:
|
try:
|
||||||
@ -1375,3 +1392,26 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount=amount, fee_abs=fee_abs)
|
amount=amount, fee_abs=fee_abs)
|
||||||
else:
|
else:
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
def get_valid_price(self, custom_price: float, proposed_price: float) -> float:
|
||||||
|
"""
|
||||||
|
Return the valid price.
|
||||||
|
Check if the custom price is of the good type if not return proposed_price
|
||||||
|
:return: valid price for the order
|
||||||
|
"""
|
||||||
|
if custom_price:
|
||||||
|
try:
|
||||||
|
valid_custom_price = float(custom_price)
|
||||||
|
except ValueError:
|
||||||
|
valid_custom_price = proposed_price
|
||||||
|
else:
|
||||||
|
valid_custom_price = proposed_price
|
||||||
|
|
||||||
|
cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02)
|
||||||
|
min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r)
|
||||||
|
max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r)
|
||||||
|
|
||||||
|
# Bracket between min_custom_price_allowed and max_custom_price_allowed
|
||||||
|
return max(
|
||||||
|
min(valid_custom_price, max_custom_price_allowed),
|
||||||
|
min_custom_price_allowed)
|
||||||
|
@ -280,6 +280,43 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
return self.stoploss
|
return self.stoploss
|
||||||
|
|
||||||
|
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
||||||
|
**kwargs) -> float:
|
||||||
|
"""
|
||||||
|
Custom entry price logic, returning the new entry price.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns None, orderbook is used to set entry price
|
||||||
|
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return float: New entry price value if provided
|
||||||
|
"""
|
||||||
|
return proposed_rate
|
||||||
|
|
||||||
|
def custom_exit_price(self, pair: str, trade: Trade,
|
||||||
|
current_time: datetime, proposed_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
"""
|
||||||
|
Custom exit price logic, returning the new exit price.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns None, orderbook is used to set exit price
|
||||||
|
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param trade: trade object.
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||||
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return float: New exit price value if provided
|
||||||
|
"""
|
||||||
|
return proposed_rate
|
||||||
|
|
||||||
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
|
current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
|
||||||
"""
|
"""
|
||||||
|
@ -904,6 +904,40 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
|
|||||||
with pytest.raises(PricingError, match="Could not determine buy price."):
|
with pytest.raises(PricingError, match="Could not determine buy price."):
|
||||||
freqtrade.execute_buy(pair, stake_amount)
|
freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
|
||||||
|
# In case of custom entry price
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.50)
|
||||||
|
limit_buy_order['status'] = 'open'
|
||||||
|
limit_buy_order['id'] = '5566'
|
||||||
|
freqtrade.strategy.custom_entry_price = lambda **kwargs: 0.508
|
||||||
|
assert freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
trade = Trade.query.all()[6]
|
||||||
|
assert trade
|
||||||
|
assert trade.open_rate_requested == 0.508
|
||||||
|
|
||||||
|
# In case of custom entry price set to None
|
||||||
|
limit_buy_order['status'] = 'open'
|
||||||
|
limit_buy_order['id'] = '5567'
|
||||||
|
freqtrade.strategy.custom_entry_price = lambda **kwargs: None
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_rate=MagicMock(return_value=10),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
trade = Trade.query.all()[7]
|
||||||
|
assert trade
|
||||||
|
assert trade.open_rate_requested == 10
|
||||||
|
|
||||||
|
# In case of custom entry price not float type
|
||||||
|
limit_buy_order['status'] = 'open'
|
||||||
|
limit_buy_order['id'] = '5568'
|
||||||
|
freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price"
|
||||||
|
assert freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
trade = Trade.query.all()[8]
|
||||||
|
assert trade
|
||||||
|
assert trade.open_rate_requested == 10
|
||||||
|
|
||||||
|
|
||||||
def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None:
|
def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None:
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
@ -2715,6 +2749,70 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
|
|||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_sell_custom_exit_price(default_conf, ticker, fee, ticker_sell_up, mocker) -> None:
|
||||||
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
fetch_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
_is_dry_limit_order_filled=MagicMock(return_value=False),
|
||||||
|
)
|
||||||
|
patch_whitelist(mocker, default_conf)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False)
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtrade.enter_positions()
|
||||||
|
rpc_mock.reset_mock()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
assert freqtrade.strategy.confirm_trade_exit.call_count == 0
|
||||||
|
|
||||||
|
# Increase the price and sell it
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
fetch_ticker=ticker_sell_up
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
|
||||||
|
|
||||||
|
# Set a custom exit price
|
||||||
|
freqtrade.strategy.custom_exit_price = lambda **kwargs: 1.170e-05
|
||||||
|
|
||||||
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
|
||||||
|
sell_reason=SellCheckTuple(sell_type=SellType.SELL_SIGNAL))
|
||||||
|
|
||||||
|
# Sell price must be different to default bid price
|
||||||
|
|
||||||
|
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
|
||||||
|
|
||||||
|
assert rpc_mock.call_count == 1
|
||||||
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
|
assert {
|
||||||
|
'trade_id': 1,
|
||||||
|
'type': RPCMessageType.SELL,
|
||||||
|
'exchange': 'Binance',
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'gain': 'profit',
|
||||||
|
'limit': 1.170e-05,
|
||||||
|
'amount': 91.07468123,
|
||||||
|
'order_type': 'limit',
|
||||||
|
'open_rate': 1.098e-05,
|
||||||
|
'current_rate': 1.173e-05,
|
||||||
|
'profit_amount': 6.041e-05,
|
||||||
|
'profit_ratio': 0.06025919,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'fiat_currency': 'USD',
|
||||||
|
'sell_reason': SellType.SELL_SIGNAL.value,
|
||||||
|
'open_date': ANY,
|
||||||
|
'close_date': ANY,
|
||||||
|
'close_rate': ANY,
|
||||||
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee,
|
def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee,
|
||||||
ticker_sell_down, mocker) -> None:
|
ticker_sell_down, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
@ -4491,3 +4589,43 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog):
|
|||||||
|
|
||||||
freqtrade.refind_lost_order(trades[4])
|
freqtrade.refind_lost_order(trades[4])
|
||||||
assert log_has(f"Error updating {order['id']}.", caplog)
|
assert log_has(f"Error updating {order['id']}.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_valid_price(mocker, default_conf) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.config['custom_price_max_distance_ratio'] = 0.02
|
||||||
|
|
||||||
|
custom_price_string = "10"
|
||||||
|
custom_price_badstring = "10abc"
|
||||||
|
custom_price_float = 10.0
|
||||||
|
custom_price_int = 10
|
||||||
|
|
||||||
|
custom_price_over_max_alwd = 11.0
|
||||||
|
custom_price_under_min_alwd = 9.0
|
||||||
|
proposed_price = 10.1
|
||||||
|
|
||||||
|
valid_price_from_string = freqtrade.get_valid_price(custom_price_string, proposed_price)
|
||||||
|
valid_price_from_badstring = freqtrade.get_valid_price(custom_price_badstring, proposed_price)
|
||||||
|
valid_price_from_int = freqtrade.get_valid_price(custom_price_int, proposed_price)
|
||||||
|
valid_price_from_float = freqtrade.get_valid_price(custom_price_float, proposed_price)
|
||||||
|
|
||||||
|
valid_price_at_max_alwd = freqtrade.get_valid_price(custom_price_over_max_alwd, proposed_price)
|
||||||
|
valid_price_at_min_alwd = freqtrade.get_valid_price(custom_price_under_min_alwd, proposed_price)
|
||||||
|
|
||||||
|
assert isinstance(valid_price_from_string, float)
|
||||||
|
assert isinstance(valid_price_from_badstring, float)
|
||||||
|
assert isinstance(valid_price_from_int, float)
|
||||||
|
assert isinstance(valid_price_from_float, float)
|
||||||
|
|
||||||
|
assert valid_price_from_string == custom_price_float
|
||||||
|
assert valid_price_from_badstring == proposed_price
|
||||||
|
assert valid_price_from_int == custom_price_int
|
||||||
|
assert valid_price_from_float == custom_price_float
|
||||||
|
|
||||||
|
assert valid_price_at_max_alwd < custom_price_over_max_alwd
|
||||||
|
assert valid_price_at_max_alwd > proposed_price
|
||||||
|
|
||||||
|
assert valid_price_at_min_alwd > custom_price_under_min_alwd
|
||||||
|
assert valid_price_at_min_alwd < proposed_price
|
||||||
|
Loading…
Reference in New Issue
Block a user