Merge branch 'freqtrade:develop' into plot_hyperopt_stats

This commit is contained in:
Italo 2022-01-24 21:46:33 +00:00 committed by GitHub
commit a7ea06791a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 203 additions and 85 deletions

View File

@ -1,4 +1,4 @@
mkdocs==1.2.3 mkdocs==1.2.3
mkdocs-material==8.1.7 mkdocs-material==8.1.8
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.1 pymdown-extensions==9.1

View File

@ -362,8 +362,8 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def custom_entry_price(self, pair: str, current_time: datetime, def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
proposed_rate, **kwargs) -> float: entry_tag: Optional[str], **kwargs) -> float:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe) timeframe=self.timeframe)
@ -413,7 +413,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to
The function must return either `True` (cancel order) or `False` (keep order alive). The function must return either `True` (cancel order) or `False` (keep order alive).
``` python ``` python
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -426,22 +426,24 @@ class AwesomeStrategy(IStrategy):
'sell': 60 * 25 'sell': 60 * 25
} }
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict,
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True return True
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
return True return True
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
return True return True
return False return False
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True return True
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
return True return True
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
return True return True
return False return False
``` ```
@ -500,7 +502,8 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, **kwargs) -> bool: time_in_force: str, current_time: datetime, entry_tag: Optional[str],
**kwargs) -> bool:
""" """
Called right before placing a buy order. Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
@ -618,7 +621,7 @@ class DigDeeperStrategy(IStrategy):
# This is called when placing the initial order (opening trade) # This is called when placing the initial order (opening trade)
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float, proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float: entry_tag: Optional[str], **kwargs) -> float:
# We need to leave most of the funds for possible further DCA orders # We need to leave most of the funds for possible further DCA orders
# This also applies to fixed stakes # This also applies to fixed stakes

View File

@ -614,7 +614,7 @@ class Exchange:
'side': side, 'side': side,
'filled': 0, 'filled': 0,
'remaining': _amount, 'remaining': _amount,
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'timestamp': arrow.utcnow().int_timestamp * 1000, 'timestamp': arrow.utcnow().int_timestamp * 1000,
'status': "closed" if ordertype == "market" else "open", 'status': "closed" if ordertype == "market" else "open",
'fee': None, 'fee': None,

View File

@ -9,8 +9,6 @@ from math import isclose
from threading import Lock from threading import Lock
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import arrow
from freqtrade import __version__, constants from freqtrade import __version__, constants
from freqtrade.configuration import validate_config_consistency from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
@ -533,7 +531,7 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None pos_adjust = trade is not None
enter_limit_requested, stake_amount = self.get_valid_enter_price_and_stake( enter_limit_requested, stake_amount = self.get_valid_enter_price_and_stake(
pair, price, stake_amount, trade) pair, price, stake_amount, buy_tag, trade)
if not stake_amount: if not stake_amount:
return False return False
@ -552,7 +550,8 @@ class FreqtradeBot(LoggingMixin):
if not pos_adjust and not strategy_safe_wrapper( if not pos_adjust and not strategy_safe_wrapper(
self.strategy.confirm_trade_entry, default_retval=True)( self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
entry_tag=buy_tag):
logger.info(f"User requested abortion of buying {pair}") logger.info(f"User requested abortion of buying {pair}")
return False return False
amount = self.exchange.amount_to_precision(pair, amount) amount = self.exchange.amount_to_precision(pair, amount)
@ -662,6 +661,7 @@ class FreqtradeBot(LoggingMixin):
def get_valid_enter_price_and_stake( def get_valid_enter_price_and_stake(
self, pair: str, price: Optional[float], stake_amount: float, self, pair: str, price: Optional[float], stake_amount: float,
entry_tag: Optional[str],
trade: Optional[Trade]) -> Tuple[float, float]: trade: Optional[Trade]) -> Tuple[float, float]:
if price: if price:
enter_limit_requested = price enter_limit_requested = price
@ -671,7 +671,7 @@ class FreqtradeBot(LoggingMixin):
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_enter_rate)( default_retval=proposed_enter_rate)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
proposed_rate=proposed_enter_rate) proposed_rate=proposed_enter_rate, entry_tag=entry_tag)
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
if not enter_limit_requested: if not enter_limit_requested:
@ -684,7 +684,7 @@ class FreqtradeBot(LoggingMixin):
default_retval=stake_amount)( default_retval=stake_amount)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested, proposed_stake=stake_amount, current_rate=enter_limit_requested, proposed_stake=stake_amount,
min_stake=min_stake_amount, max_stake=max_stake_amount) min_stake=min_stake_amount, max_stake=max_stake_amount, entry_tag=entry_tag)
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
return enter_limit_requested, stake_amount return enter_limit_requested, stake_amount
@ -959,20 +959,6 @@ class FreqtradeBot(LoggingMixin):
return True return True
return False return False
def _check_timed_out(self, side: str, order: dict) -> bool:
"""
Check if timeout is active, and if the order is still open and timed out
"""
timeout = self.config.get('unfilledtimeout', {}).get(side)
ordertime = arrow.get(order['datetime']).datetime
if timeout is not None:
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
timeout_kwargs = {timeout_unit: -timeout}
timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime
return (order['status'] == 'open' and order['side'] == side
and ordertime < timeout_threshold)
return False
def check_handle_timedout(self) -> None: def check_handle_timedout(self) -> None:
""" """
Check if any orders are timed out and cancel if necessary Check if any orders are timed out and cancel if necessary
@ -993,20 +979,16 @@ class FreqtradeBot(LoggingMixin):
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled fully_cancelled
or self._check_timed_out('buy', order) or self.strategy.ft_check_timed_out(
or strategy_safe_wrapper(self.strategy.check_buy_timeout, 'buy', trade, order, datetime.now(timezone.utc))
default_retval=False)(pair=trade.pair, )):
trade=trade,
order=order))):
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled fully_cancelled
or self._check_timed_out('sell', order) or self.strategy.ft_check_timed_out(
or strategy_safe_wrapper(self.strategy.check_sell_timeout, 'sell', trade, order, datetime.now(timezone.utc)))
default_retval=False)(pair=trade.pair, ):
trade=trade,
order=order))):
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
canceled_count = trade.get_exit_order_count() canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)

View File

@ -463,11 +463,13 @@ class Backtesting:
def _enter_trade(self, pair: str, row: Tuple, stake_amount: Optional[float] = None, def _enter_trade(self, pair: str, row: Tuple, stake_amount: Optional[float] = None,
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[BUY_TAG_IDX] if len(row) >= BUY_TAG_IDX + 1 else None
# let's call the custom entry price, using the open price as default price # let's call the custom entry price, using the open price as default price
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=row[OPEN_IDX])( default_retval=row[OPEN_IDX])(
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), pair=pair, current_time=current_time,
proposed_rate=row[OPEN_IDX]) # default value is the open rate proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate
# Move rate to within the candle's low/high rate # Move rate to within the candle's low/high rate
propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX]) propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX])
@ -484,8 +486,9 @@ class Backtesting:
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)( default_retval=stake_amount)(
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate, pair=pair, current_time=current_time, current_rate=propose_rate,
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount,
entry_tag=entry_tag)
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
@ -500,24 +503,24 @@ class Backtesting:
if not pos_adjust: if not pos_adjust:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate, pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): time_in_force=time_in_force, current_time=current_time,
entry_tag=entry_tag):
return None return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
amount = round(stake_amount / propose_rate, 8) amount = round(stake_amount / propose_rate, 8)
if trade is None: if trade is None:
# Enter trade # Enter trade
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
trade = LocalTrade( trade = LocalTrade(
pair=pair, pair=pair,
open_rate=propose_rate, open_rate=propose_rate,
open_date=row[DATE_IDX].to_pydatetime(), open_date=current_time,
stake_amount=stake_amount, stake_amount=stake_amount,
amount=amount, amount=amount,
fee_open=self.fee, fee_open=self.fee,
fee_close=self.fee, fee_close=self.fee,
is_open=True, is_open=True,
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, buy_tag=entry_tag,
exchange='backtesting', exchange='backtesting',
orders=[] orders=[]
) )
@ -531,6 +534,9 @@ class Backtesting:
side="buy", side="buy",
order_type="market", order_type="market",
status="closed", status="closed",
order_date=current_time,
order_filled_date=current_time,
order_update_date=current_time,
price=propose_rate, price=propose_rate,
average=propose_rate, average=propose_rate,
amount=amount, amount=amount,

View File

@ -577,18 +577,19 @@ class LocalTrade():
total_amount = 0.0 total_amount = 0.0
total_stake = 0.0 total_stake = 0.0
for temp_order in self.orders: for o in self.orders:
if (temp_order.ft_is_open or if (o.ft_is_open or
(temp_order.ft_order_side != 'buy') or (o.ft_order_side != 'buy') or
(temp_order.status not in NON_OPEN_EXCHANGE_STATES)): (o.status not in NON_OPEN_EXCHANGE_STATES)):
continue continue
tmp_amount = temp_order.amount tmp_amount = o.amount
if temp_order.filled is not None: tmp_price = o.average or o.price
tmp_amount = temp_order.filled if o.filled is not None:
if tmp_amount > 0.0 and temp_order.average is not None: tmp_amount = o.filled
if tmp_amount > 0.0 and tmp_price is not None:
total_amount += tmp_amount total_amount += tmp_amount
total_stake += temp_order.average * tmp_amount total_stake += tmp_price * tmp_amount
if total_amount > 0: if total_amount > 0:
self.open_rate = total_stake / total_amount self.open_rate = total_stake / total_amount

View File

@ -188,7 +188,17 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return dataframe return dataframe
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass
def check_buy_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
""" """
Check buy timeout function callback. Check buy timeout function callback.
This method can be used to override the buy-timeout. This method can be used to override the buy-timeout.
@ -201,12 +211,14 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair the trade is for :param pair: Pair the trade is for
:param trade: trade object. :param trade: trade object.
:param order: Order dictionary as returned from CCXT. :param order: Order dictionary as returned from CCXT.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is cancelled. :return bool: When True is returned, then the buy-order is cancelled.
""" """
return False return False
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
""" """
Check sell timeout function callback. Check sell timeout function callback.
This method can be used to override the sell-timeout. This method can be used to override the sell-timeout.
@ -219,22 +231,15 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair the trade is for :param pair: Pair the trade is for
:param trade: trade object. :param trade: trade object.
:param order: Order dictionary as returned from CCXT. :param order: Order dictionary as returned from CCXT.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is cancelled. :return bool: When True is returned, then the sell-order is cancelled.
""" """
return False return False
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, **kwargs) -> bool: time_in_force: str, current_time: datetime, entry_tag: Optional[str],
**kwargs) -> bool:
""" """
Called right before placing a buy order. Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
@ -250,6 +255,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange. :return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process False aborts the process
@ -307,7 +313,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return self.stoploss return self.stoploss
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
**kwargs) -> float: entry_tag: Optional[str], **kwargs) -> float:
""" """
Custom entry price logic, returning the new entry price. Custom entry price logic, returning the new entry price.
@ -318,6 +324,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair that's currently analyzed :param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy. :param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param **kwargs: Ensure to keep this here so updates to this won't break your 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 float: New entry price value if provided
""" """
@ -369,7 +376,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float, proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float: entry_tag: Optional[str], **kwargs) -> float:
""" """
Customize stake size for each new trade. This method is not called when edge module is Customize stake size for each new trade. This method is not called when edge module is
enabled. enabled.
@ -380,6 +387,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param proposed_stake: A stake amount proposed by the bot. :param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading. :param max_stake: Balance available for trading.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:return: A stake size, which is between min_stake and max_stake. :return: A stake size, which is between min_stake and max_stake.
""" """
return proposed_stake return proposed_stake
@ -390,6 +398,7 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be increased. Custom trade adjustment logic, returning the stake amount that a trade should be increased.
This means extra buy orders with additional fees. This means extra buy orders with additional fees.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
@ -852,6 +861,29 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return current_profit > roi return current_profit > roi
def ft_check_timed_out(self, side: str, trade: Trade, order: Dict,
current_time: datetime) -> bool:
"""
FT Internal method.
Check if timeout is active, and if the order is still open and timed out
"""
timeout = self.config.get('unfilledtimeout', {}).get(side)
ordertime = arrow.get(order['datetime']).datetime
if timeout is not None:
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
timeout_kwargs = {timeout_unit: -timeout}
timeout_threshold = current_time + timedelta(**timeout_kwargs)
timedout = (order['status'] == 'open' and order['side'] == side
and ordertime < timeout_threshold)
if timedout:
return True
time_method = self.check_sell_timeout if order['side'] == 'sell' else self.check_buy_timeout
return strategy_safe_wrapper(time_method,
default_retval=False)(
pair=trade.pair, trade=trade, order=order,
current_time=current_time)
def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
""" """
Populates indicators for given candle (OHLCV) data (for multiple pairs) Populates indicators for given candle (OHLCV) data (for multiple pairs)

View File

@ -12,9 +12,47 @@ def bot_loop_start(self, **kwargs) -> None:
""" """
pass pass
def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: float,
entry_tag: 'Optional[str]', **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 entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
: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_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float, def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float, proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float: entry_tag: 'Optional[str]', **kwargs) -> float:
""" """
Customize stake size for each new trade. This method is not called when edge module is Customize stake size for each new trade. This method is not called when edge module is
enabled. enabled.
@ -25,6 +63,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate:
:param proposed_stake: A stake amount proposed by the bot. :param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading. :param max_stake: Balance available for trading.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:return: A stake size, which is between min_stake and max_stake. :return: A stake size, which is between min_stake and max_stake.
""" """
return proposed_stake return proposed_stake
@ -78,7 +117,8 @@ def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', curre
return None return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: 'datetime', **kwargs) -> bool: time_in_force: str, current_time: 'datetime', entry_tag: 'Optional[str]',
**kwargs) -> bool:
""" """
Called right before placing a buy order. Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
@ -94,6 +134,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange. :return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process False aborts the process
@ -167,3 +208,26 @@ def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -
:return bool: When True is returned, then the sell-order is cancelled. :return bool: When True is returned, then the sell-order is cancelled.
""" """
return False return False
def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
current_rate: float, current_profit: float, min_stake: float,
max_stake: float, **kwargs) -> 'Optional[float]':
"""
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
This means extra buy orders with additional fees.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns None
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade
"""
return None

View File

@ -8,7 +8,7 @@ flake8==4.0.1
flake8-tidy-imports==4.6.0 flake8-tidy-imports==4.6.0
mypy==0.931 mypy==0.931
pytest==6.2.5 pytest==6.2.5
pytest-asyncio==0.17.1 pytest-asyncio==0.17.2
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-mock==3.6.1 pytest-mock==3.6.1
pytest-random-order==1.0.4 pytest-random-order==1.0.4
@ -21,9 +21,9 @@ nbconvert==6.4.0
# mypy types # mypy types
types-cachetools==4.2.9 types-cachetools==4.2.9
types-filelock==3.2.4 types-filelock==3.2.5
types-requests==2.27.7 types-requests==2.27.7
types-tabulate==0.8.5 types-tabulate==0.8.5
# Extensions to datetime library # Extensions to datetime library
types-python-dateutil==2.8.7 types-python-dateutil==2.8.8

View File

@ -7,7 +7,7 @@ ccxt==1.68.20
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==36.0.1 cryptography==36.0.1
aiohttp==3.8.1 aiohttp==3.8.1
SQLAlchemy==1.4.29 SQLAlchemy==1.4.31
python-telegram-bot==13.10 python-telegram-bot==13.10
arrow==1.2.1 arrow==1.2.1
cachetools==4.2.2 cachetools==4.2.2
@ -32,7 +32,7 @@ python-rapidjson==1.5
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.72.0 fastapi==0.73.0
uvicorn==0.17.0 uvicorn==0.17.0
pyjwt==2.3.0 pyjwt==2.3.0
aiofiles==0.8.0 aiofiles==0.8.0

View File

@ -763,6 +763,8 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
# While buy-signals are unrealistic, running backtesting # While buy-signals are unrealistic, running backtesting
# over and over again should not cause different results # over and over again should not cause different results
for [contour, numres] in tests: for [contour, numres] in tests:
# Debug output for random test failure
print(f"{contour}, {numres}")
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres

View File

@ -37,7 +37,7 @@ def test_strategy_test_v2(result, fee):
assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', rate=20000, time_in_force='gtc',
current_time=datetime.utcnow()) is True current_time=datetime.utcnow(), entry_tag=None) is True
assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', sell_reason='roi', rate=20000, time_in_force='gtc', sell_reason='roi',
current_time=datetime.utcnow()) is True current_time=datetime.utcnow()) is True

View File

@ -243,6 +243,8 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert len(trade.orders) == 2 assert len(trade.orders) == 2
for o in trade.orders:
assert o.status == "closed"
assert trade.stake_amount == 120 assert trade.stake_amount == 120
# Open-rate averaged between 2.0 and 2.0 * 0.995 # Open-rate averaged between 2.0 and 2.0 * 0.995
@ -258,7 +260,6 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert trade.orders[1].amount == 60 / ticker_usdt_modif['bid'] assert trade.orders[1].amount == 60 / ticker_usdt_modif['bid']
assert trade.amount == trade.orders[0].amount + trade.orders[1].amount assert trade.amount == trade.orders[0].amount + trade.orders[1].amount
assert trade.nr_of_successful_buys == 2 assert trade.nr_of_successful_buys == 2
# Sell # Sell

View File

@ -1661,6 +1661,33 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
assert trade.fee_open_cost == 2 * o1_fee_cost assert trade.fee_open_cost == 2 * o1_fee_cost
assert trade.open_trade_value == 2 * o1_trade_val assert trade.open_trade_value == 2 * o1_trade_val
assert trade.nr_of_successful_buys == 2 assert trade.nr_of_successful_buys == 2
# Check with 1 order
order_noavg = Order(
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=False,
status="closed",
symbol=trade.pair,
order_type="market",
side="buy",
price=o1_rate,
average=None,
filled=o1_amount,
remaining=0,
cost=o1_amount,
order_date=trade.open_date,
order_filled_date=trade.open_date,
)
trade.orders.append(order_noavg)
trade.recalc_trade_from_orders()
# Calling recalc with single initial order should not change anything
assert trade.amount == 3 * o1_amount
assert trade.stake_amount == 3 * o1_amount
assert trade.open_rate == o1_rate
assert trade.fee_open_cost == 3 * o1_fee_cost
assert trade.open_trade_value == 3 * o1_trade_val
assert trade.nr_of_successful_buys == 3
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")