add feature custom entry price for live
This commit is contained in:
parent
dfc17f2bd1
commit
f11f5d17e9
@ -357,6 +357,33 @@ See [Dataframe access](#dataframe-access) for more information about dataframe u
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Custom order entry price rules
|
||||||
|
|
||||||
|
By default, freqtrade use the orderbook to automatically set an order price, you also have the option to create custom order prices based on your strategy.
|
||||||
|
|
||||||
|
You can use this feature by setting the `use_custom_entry_price` option to `true` in config and creating a custom_entry_price function.
|
||||||
|
|
||||||
|
### Custom order entry price exemple
|
||||||
|
``` 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,
|
||||||
|
current_rate, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
|
timeframe=self.timeframe)
|
||||||
|
entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
||||||
|
|
||||||
|
return entryprice
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
||||||
@ -366,7 +393,7 @@ However, freqtrade also offers a custom callback for both order types, which all
|
|||||||
!!! Note
|
!!! Note
|
||||||
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
|
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
|
||||||
|
|
||||||
### Custom order timeout example
|
## Custom order timeout example
|
||||||
|
|
||||||
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
|
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
|
||||||
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
|
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
|
||||||
|
@ -433,6 +433,8 @@ SCHEMA_MINIMAL_REQUIRED = [
|
|||||||
|
|
||||||
CANCEL_REASON = {
|
CANCEL_REASON = {
|
||||||
"TIMEOUT": "cancelled due to timeout",
|
"TIMEOUT": "cancelled due to timeout",
|
||||||
|
"ENTRYPRICECHANGED": "Custom entry price changed",
|
||||||
|
"EXITPRICECHANGED": "Custom exit price changed",
|
||||||
"PARTIALLY_FILLED_KEEP_OPEN": "partially filled - keeping order open",
|
"PARTIALLY_FILLED_KEEP_OPEN": "partially filled - keeping order open",
|
||||||
"PARTIALLY_FILLED": "partially filled",
|
"PARTIALLY_FILLED": "partially filled",
|
||||||
"FULLY_CANCELLED": "fully cancelled",
|
"FULLY_CANCELLED": "fully cancelled",
|
||||||
|
@ -169,6 +169,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
with self._sell_lock:
|
with self._sell_lock:
|
||||||
# Check and handle any timed out open orders
|
# Check and handle any timed out open orders
|
||||||
self.check_handle_timedout()
|
self.check_handle_timedout()
|
||||||
|
self.check_handle_custom_entryprice_outdated()
|
||||||
|
|
||||||
# Protect from collisions with forcesell.
|
# Protect from collisions with forcesell.
|
||||||
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
||||||
@ -480,6 +481,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
else:
|
else:
|
||||||
# Calculate price
|
# Calculate price
|
||||||
buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy")
|
buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||||
|
if self.config.get('use_custom_entry_price', False):
|
||||||
|
buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||||
|
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||||
|
default_retval=stake_amount)(
|
||||||
|
pair=pair, current_time=datetime.now(timezone.utc),
|
||||||
|
current_rate=buy_rate)
|
||||||
|
|
||||||
|
buy_limit_requested = custom_entry_price
|
||||||
|
|
||||||
if not buy_limit_requested:
|
if not buy_limit_requested:
|
||||||
raise PricingError('Could not determine buy price.')
|
raise PricingError('Could not determine buy price.')
|
||||||
@ -911,6 +920,70 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order=order))):
|
order=order))):
|
||||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
|
|
||||||
|
def _check_entryprice_outdated(self, side: str, order: dict) -> bool:
|
||||||
|
"""
|
||||||
|
Check if entry price is outdated by comparing it to the new prefered entry price
|
||||||
|
, and if the order is still open and price outdated
|
||||||
|
"""
|
||||||
|
#print("check_entryprice_outdated")
|
||||||
|
if self.config.get('use_custom_entry_price', False):
|
||||||
|
order_prefered_entry_price = order['price'] # order['trade']
|
||||||
|
|
||||||
|
#print(order)
|
||||||
|
#order_open_rate_requested = order.trade['open_rate_requested']
|
||||||
|
#print("order_trade_object : {}".format(order['trade']))
|
||||||
|
|
||||||
|
# get pep from strategy data provider
|
||||||
|
pair = order['symbol']
|
||||||
|
old_prefered_entry_price = order_prefered_entry_price
|
||||||
|
#new_prefered_entry_price = self.strategy.custom_info[pair]['pep_long'].iloc[-1] #buy_limit_requested
|
||||||
|
new_prefered_entry_price = self.strategy.entryprice
|
||||||
|
|
||||||
|
old_prefered_entry_price_rounded = self.exchange.price_to_precision(pair, order_prefered_entry_price)
|
||||||
|
new_prefered_entry_price_rounded = self.exchange.price_to_precision(pair, new_prefered_entry_price)
|
||||||
|
|
||||||
|
if old_prefered_entry_price_rounded != new_prefered_entry_price_rounded:
|
||||||
|
print("order['symbol']: {}".format(order['symbol']))
|
||||||
|
print("new_prefered_entry_price: {}, old_prefered_entry_price: {}".format(new_prefered_entry_price, old_prefered_entry_price))
|
||||||
|
print("rounded new pep: {}, rounded old pep: {}".format(new_prefered_entry_price_rounded, old_prefered_entry_price_rounded))
|
||||||
|
print("Delta in prefered entry price, order to cancel")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_handle_custom_entryprice_outdated(self) -> None:
|
||||||
|
"""
|
||||||
|
Check if any orders prefered entryprice change and cancel if necessary
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
for trade in Trade.get_open_order_trades():
|
||||||
|
try:
|
||||||
|
if not trade.open_order_id:
|
||||||
|
continue
|
||||||
|
order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||||
|
except (ExchangeError):
|
||||||
|
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||||
|
continue
|
||||||
|
|
||||||
|
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
|
||||||
|
|
||||||
|
# Refresh entryprice value if order is open
|
||||||
|
if (order['status'] == 'open'):
|
||||||
|
self.strategy.entryprice = strategy_safe_wrapper(self.strategy.custom_entry_price)(
|
||||||
|
pair=trade.pair, current_time=datetime.now(timezone.utc),
|
||||||
|
current_rate=trade.open_rate_requested)
|
||||||
|
|
||||||
|
if (order['side'] == 'buy' and (order['status'] == 'open') and (
|
||||||
|
self._check_entryprice_outdated('buy', order))):
|
||||||
|
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ENTRYPRICECHANGED'])
|
||||||
|
|
||||||
|
elif (order['side'] == 'sell' and (order['status'] == 'open') and (
|
||||||
|
self._check_entryprice_outdated('sell', order))):
|
||||||
|
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['EXITPRICECHANGED'])
|
||||||
|
|
||||||
def cancel_all_open_orders(self) -> None:
|
def cancel_all_open_orders(self) -> None:
|
||||||
"""
|
"""
|
||||||
Cancel all orders that are currently open
|
Cancel all orders that are currently open
|
||||||
|
@ -79,6 +79,7 @@ class StrategyResolver(IResolver):
|
|||||||
("trailing_stop_positive_offset", 0.0),
|
("trailing_stop_positive_offset", 0.0),
|
||||||
("trailing_only_offset_is_reached", None),
|
("trailing_only_offset_is_reached", None),
|
||||||
("use_custom_stoploss", None),
|
("use_custom_stoploss", None),
|
||||||
|
("use_custom_entry_price", None),
|
||||||
("process_only_new_candles", None),
|
("process_only_new_candles", None),
|
||||||
("order_types", None),
|
("order_types", None),
|
||||||
("order_time_in_force", None),
|
("order_time_in_force", None),
|
||||||
|
@ -129,6 +129,7 @@ class ShowConfig(BaseModel):
|
|||||||
trailing_stop_positive_offset: Optional[float]
|
trailing_stop_positive_offset: Optional[float]
|
||||||
trailing_only_offset_is_reached: Optional[bool]
|
trailing_only_offset_is_reached: Optional[bool]
|
||||||
use_custom_stoploss: Optional[bool]
|
use_custom_stoploss: Optional[bool]
|
||||||
|
use_custom_entry_price: Optional[bool]
|
||||||
timeframe: Optional[str]
|
timeframe: Optional[str]
|
||||||
timeframe_ms: int
|
timeframe_ms: int
|
||||||
timeframe_min: int
|
timeframe_min: int
|
||||||
|
@ -116,6 +116,7 @@ class RPC:
|
|||||||
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
||||||
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
|
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
|
||||||
'use_custom_stoploss': config.get('use_custom_stoploss'),
|
'use_custom_stoploss': config.get('use_custom_stoploss'),
|
||||||
|
'use_custom_entry_price': config.get('use_custom_entry_price'),
|
||||||
'bot_name': config.get('bot_name', 'freqtrade'),
|
'bot_name': config.get('bot_name', 'freqtrade'),
|
||||||
'timeframe': config.get('timeframe'),
|
'timeframe': config.get('timeframe'),
|
||||||
'timeframe_ms': timeframe_to_msecs(config['timeframe']
|
'timeframe_ms': timeframe_to_msecs(config['timeframe']
|
||||||
|
@ -69,6 +69,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
# associated stoploss
|
# associated stoploss
|
||||||
stoploss: float
|
stoploss: float
|
||||||
|
|
||||||
|
# custom order price
|
||||||
|
entryprice: Optional[float] = None
|
||||||
|
exitprice: Optional[float] = None
|
||||||
|
|
||||||
# trailing stoploss
|
# trailing stoploss
|
||||||
trailing_stop: bool = False
|
trailing_stop: bool = False
|
||||||
trailing_stop_positive: Optional[float] = None
|
trailing_stop_positive: Optional[float] = None
|
||||||
@ -280,6 +284,24 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
return self.stoploss
|
return self.stoploss
|
||||||
|
|
||||||
|
def custom_entry_price(self, pair: str, current_time: datetime, current_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
|
||||||
|
Only called when use_custom_entry_price is set to True.
|
||||||
|
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param current_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 self.entryprice
|
||||||
|
|
||||||
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]]:
|
||||||
"""
|
"""
|
||||||
@ -635,6 +657,42 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
# logger.debug(f"{trade.pair} - No sell signal.")
|
# logger.debug(f"{trade.pair} - No sell signal.")
|
||||||
return SellCheckTuple(sell_type=SellType.NONE)
|
return SellCheckTuple(sell_type=SellType.NONE)
|
||||||
|
|
||||||
|
def entry_price_reached(self, pair: str, current_rate: float,
|
||||||
|
current_time: datetime, low: float = None,
|
||||||
|
high: float = None, side: str = "long") -> bool:
|
||||||
|
"""
|
||||||
|
Based on current candle low ,decides if entry price was reached
|
||||||
|
:param current_rate: current rate
|
||||||
|
:param low: Low value of this candle, only set in backtesting
|
||||||
|
:param high: High value of this candle, only set in backtesting
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.use_custom_entry_price:
|
||||||
|
entry_price_value = strategy_safe_wrapper(self.custom_entry_price, default_retval=None
|
||||||
|
)(pair=pair,
|
||||||
|
current_time=current_time,
|
||||||
|
current_rate=current_rate)
|
||||||
|
# Sanity check - error cases will return None
|
||||||
|
if side == "long":
|
||||||
|
if entry_price_value > low:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.info(f"Entry failed because entry price {entry_price_value} \
|
||||||
|
higher than candle low in long side")
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif side == "short":
|
||||||
|
if entry_price_value < high:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.info(f"Entry failed because entry price {entry_price_value} \
|
||||||
|
higher than candle high in short side")
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warning("CustomEntryPrice function did not return valid entry price")
|
||||||
|
return False
|
||||||
|
|
||||||
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
||||||
current_time: datetime, current_profit: float,
|
current_time: datetime, current_profit: float,
|
||||||
force_stoploss: float, low: float = None,
|
force_stoploss: float, low: float = None,
|
||||||
|
@ -431,6 +431,34 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
|
|||||||
strategy.custom_stoploss = original_stopvalue
|
strategy.custom_stoploss = original_stopvalue
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'current_rate, exp_custom_entry', 'expected_result', 'use_custom_entry_price', 'custom_entry' [
|
||||||
|
# Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing,
|
||||||
|
# enable custom stoploss, expected after 1st call, expected after 2nd call
|
||||||
|
(99, 98, False, True, lambda current_rate, **kwargs: current_rate - (current_rate * 0.01)), # custom_entry_price pice - (price * 0.01)
|
||||||
|
(97.8, 98, True, True, lambda current_rate, **kwargs: current_rate - (current_rate * 0.01)), # price stayed under entry price
|
||||||
|
(97.8, 98, True, True, lambda current_rate, **kwargs: current_rate + (current_rate * 0.01)), # entry price over current price
|
||||||
|
(99.9, 98, True, False, None), # feature not activated
|
||||||
|
])
|
||||||
|
def test_entry_price_reached(default_conf, current_rate, exp_custom_entry, candle_ohlc,
|
||||||
|
expected_result, use_custom_entry_price, custom_entry) -> None:
|
||||||
|
|
||||||
|
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||||
|
|
||||||
|
|
||||||
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
|
|
||||||
|
strategy.use_custom_entry_price = use_custom_entry_price
|
||||||
|
custom_entry_price = custom_entry
|
||||||
|
if use_custom_entry_price:
|
||||||
|
strategy.custom_entry_price = custom_entry(current_rate)
|
||||||
|
|
||||||
|
now = arrow.utcnow().datetime
|
||||||
|
entry_flag = strategy.entry_price_reached(current_rate=current_rate, low= None, high=None)
|
||||||
|
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
def test_custom_sell(default_conf, fee, caplog) -> None:
|
def test_custom_sell(default_conf, fee, caplog) -> None:
|
||||||
|
|
||||||
default_conf.update({'strategy': 'DefaultStrategy'})
|
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||||
|
@ -904,6 +904,36 @@ 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)
|
||||||
|
|
||||||
|
def test_execute_buy_custom_entry_price(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
default_conf.update({'use_custom_entry_price': True})
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
|
||||||
|
stake_amount = 3
|
||||||
|
bid = 2304
|
||||||
|
buy_rate_mock = MagicMock(return_value=bid)
|
||||||
|
buy_mm = MagicMock(return_value=limit_buy_order_open)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_rate=buy_rate_mock,
|
||||||
|
fetch_ticker=MagicMock(return_value={
|
||||||
|
'bid': 2304,
|
||||||
|
'ask': 0.00001173,
|
||||||
|
'last': 2304
|
||||||
|
}),
|
||||||
|
buy=buy_mm,
|
||||||
|
get_min_pair_stake_amount=MagicMock(return_value=1),
|
||||||
|
get_fee=fee,
|
||||||
|
)
|
||||||
|
pair = 'ETH/USDT'
|
||||||
|
|
||||||
|
# Test calling with custom entry price option activated
|
||||||
|
limit_buy_order_open['id'] = '55'
|
||||||
|
assert freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
# Make sure get_rate called to provide current_rate param to custom_entry_price
|
||||||
|
assert buy_rate_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user