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.
|
||||
* Verifies existing positions and eventually places sell orders.
|
||||
* 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.
|
||||
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
||||
* 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.
|
||||
|
||||
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
|
||||
| `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
|
||||
| `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.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
|
||||
|
@ -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
|
||||
|
||||
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']
|
||||
},
|
||||
'custom_price_max_distance_ratio': {
|
||||
'type': 'number', 'minimum': 0.0
|
||||
},
|
||||
'order_types': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
|
@ -479,7 +479,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
buy_limit_requested = price
|
||||
else:
|
||||
# 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:
|
||||
raise PricingError('Could not determine buy price.')
|
||||
@ -1076,6 +1082,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
and self.strategy.order_types['stoploss_on_exchange']:
|
||||
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 ...
|
||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||
try:
|
||||
@ -1375,3 +1392,26 @@ class FreqtradeBot(LoggingMixin):
|
||||
amount=amount, fee_abs=fee_abs)
|
||||
else:
|
||||
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
|
||||
|
||||
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,
|
||||
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."):
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
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,
|
||||
ticker_sell_down, mocker) -> None:
|
||||
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])
|
||||
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