Merge branch 'freqtrade:develop' into bt-metrics2

This commit is contained in:
Stefano Ariestasia
2023-09-20 17:48:52 +09:00
committed by GitHub
33 changed files with 474 additions and 104 deletions

View File

@@ -55,7 +55,7 @@ This configuration enables kraken, as well as rate-limiting to avoid bans from t
## Binance ## Binance
!!! Warning "Server location and geo-ip restrictions" !!! Warning "Server location and geo-ip restrictions"
Please be aware that binance restrict api access regarding the server country. The currents and non exhaustive countries blocked are United States, Malaysia (Singapour), Ontario (Canada). Please go to [binance terms > b. Eligibility](https://www.binance.com/en/terms) to find up to date list. Please be aware that Binance restricts API access regarding the server country. The current and non-exhaustive countries blocked are Canada, Malaysia, Netherlands and United States. Please go to [binance terms > b. Eligibility](https://www.binance.com/en/terms) to find up to date list.
Binance supports [time_in_force](configuration.md#understand-order_time_in_force). Binance supports [time_in_force](configuration.md#understand-order_time_in_force).

View File

@@ -376,7 +376,7 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from
"lookback_days": 10, "lookback_days": 10,
"min_rate_of_change": 0.01, "min_rate_of_change": 0.01,
"max_rate_of_change": 0.99, "max_rate_of_change": 0.99,
"refresh_period": 1440 "refresh_period": 86400
} }
] ]
``` ```
@@ -431,7 +431,7 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets,
"method": "RangeStabilityFilter", "method": "RangeStabilityFilter",
"lookback_days": 10, "lookback_days": 10,
"min_rate_of_change": 0.01, "min_rate_of_change": 0.01,
"refresh_period": 1440 "refresh_period": 86400
}, },
{ {
"method": "VolatilityFilter", "method": "VolatilityFilter",

View File

@@ -1,6 +1,6 @@
markdown==3.4.4 markdown==3.4.4
mkdocs==1.5.2 mkdocs==1.5.2
mkdocs-material==9.2.8 mkdocs-material==9.3.1
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.3 pymdown-extensions==10.3
jinja2==3.1.2 jinja2==3.1.2

View File

@@ -510,6 +510,9 @@ Each of these methods are called right before placing an order on the exchange.
!!! Note !!! 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. 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.
!!! Note
Using custom_entry_price, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
### Custom order entry and exit price example ### Custom order entry and exit price example
``` python ``` python
@@ -520,7 +523,7 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, def custom_entry_price(self, pair: str, trade: Optional['Trade'], current_time: datetime, proposed_rate: float,
entry_tag: Optional[str], side: str, **kwargs) -> float: entry_tag: Optional[str], side: str, **kwargs) -> float:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
@@ -823,7 +826,7 @@ class DigDeeperStrategy(IStrategy):
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be Custom trade adjustment logic, returning the stake amount that a trade should be
increased or decreased. increased or decreased.
This means extra buy or sell orders with additional fees. This means extra entry or exit orders with additional fees.
Only called when `position_adjustment_enable` is set to True. 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/
@@ -832,8 +835,9 @@ class DigDeeperStrategy(IStrategy):
:param trade: trade object. :param trade: trade object.
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate. :param current_rate: Current entry rate (same as current_entry_profit)
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate
(same as current_entry_profit).
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits) :param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits). :param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
:param current_entry_rate: Current rate using entry pricing. :param current_entry_rate: Current rate using entry pricing.

View File

@@ -280,7 +280,7 @@ After:
``` python hl_lines="3" ``` python hl_lines="3"
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, def custom_entry_price(self, pair: str, trade: Optional[Trade], current_time: datetime, proposed_rate: float,
entry_tag: Optional[str], side: str, **kwargs) -> float: entry_tag: Optional[str], side: str, **kwargs) -> float:
return proposed_rate return proposed_rate
``` ```

View File

@@ -77,7 +77,8 @@ DL_DATA_TIMEFRAMES = ['1m', '5m']
ENV_VAR_PREFIX = 'FREQTRADE__' ENV_VAR_PREFIX = 'FREQTRADE__'
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired') CANCELED_EXCHANGE_STATES = ('cancelled', 'canceled', 'expired')
NON_OPEN_EXCHANGE_STATES = CANCELED_EXCHANGE_STATES + ('closed',)
# Define decimals per coin for outputs # Define decimals per coin for outputs
# Only used for outputs. # Only used for outputs.

View File

@@ -1420,7 +1420,7 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def __fetch_orders_emulate(self, pair: str, since_ms: int) -> List[Dict]: def _fetch_orders_emulate(self, pair: str, since_ms: int) -> List[Dict]:
orders = [] orders = []
if self.exchange_has('fetchClosedOrders'): if self.exchange_has('fetchClosedOrders'):
orders = self._api.fetch_closed_orders(pair, since=since_ms) orders = self._api.fetch_closed_orders(pair, since=since_ms)
@@ -1450,9 +1450,9 @@ class Exchange:
except ccxt.NotSupported: except ccxt.NotSupported:
# Some exchanges don't support fetchOrders # Some exchanges don't support fetchOrders
# attempt to fetch open and closed orders separately # attempt to fetch open and closed orders separately
orders = self.__fetch_orders_emulate(pair, since_ms) orders = self._fetch_orders_emulate(pair, since_ms)
else: else:
orders = self.__fetch_orders_emulate(pair, since_ms) orders = self._fetch_orders_emulate(pair, since_ms)
self._log_exchange_response('fetch_orders', orders) self._log_exchange_response('fetch_orders', orders)
orders = [self._order_contracts_to_amount(o) for o in orders] orders = [self._order_contracts_to_amount(o) for o in orders]
return orders return orders

View File

@@ -1,4 +1,5 @@
import logging import logging
from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import ccxt import ccxt
@@ -10,6 +11,7 @@ from freqtrade.exceptions import (DDosProtection, OperationalException, Retryabl
from freqtrade.exchange import Exchange, date_minus_candles from freqtrade.exchange import Exchange, date_minus_candles
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
from freqtrade.misc import safe_value_fallback2 from freqtrade.misc import safe_value_fallback2
from freqtrade.util import dt_now, dt_ts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -186,7 +188,7 @@ class Okx(Exchange):
def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict: def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict:
if ( if (
order['status'] == 'closed' order.get('status', 'open') == 'closed'
and (real_order_id := order.get('info', {}).get('ordId')) is not None and (real_order_id := order.get('info', {}).get('ordId')) is not None
): ):
# Once a order triggered, we fetch the regular followup order. # Once a order triggered, we fetch the regular followup order.
@@ -240,3 +242,18 @@ class Okx(Exchange):
pair=pair, pair=pair,
params=params1, params=params1,
) )
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> List[Dict]:
orders = []
orders = self._api.fetch_closed_orders(pair, since=since_ms)
if (since_ms < dt_ts(dt_now() - timedelta(days=6, hours=23))):
# Regular fetch_closed_orders only returns 7 days of data.
# Force usage of "archive" endpoint, which returns 3 months of data.
params = {'method': 'privateGetTradeOrdersHistoryArchive'}
orders_hist = self._api.fetch_closed_orders(pair, since=since_ms, params=params)
orders.extend(orders_hist)
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
orders.extend(orders_open)
return orders

View File

@@ -263,23 +263,46 @@ class FreqaiDataDrawer:
self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy() self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy()
return return
def set_initial_return_values(self, pair: str, pred_df: DataFrame) -> None: def set_initial_return_values(self, pair: str,
pred_df: DataFrame,
dataframe: DataFrame
) -> None:
""" """
Set the initial return values to the historical predictions dataframe. This avoids needing Set the initial return values to the historical predictions dataframe. This avoids needing
to repredict on historical candles, and also stores historical predictions despite to repredict on historical candles, and also stores historical predictions despite
retrainings (so stored predictions are true predictions, not just inferencing on trained retrainings (so stored predictions are true predictions, not just inferencing on trained
data) data).
We also aim to keep the date from historical predictions so that the FreqUI displays
zeros during any downtime (between FreqAI reloads).
""" """
hist_df = self.historic_predictions new_pred = pred_df.copy()
len_diff = len(hist_df[pair].index) - len(pred_df.index) # set new_pred values to nans (we want to signal to user that there was nothing
if len_diff < 0: # historically made during downtime. The newest pred will get appeneded later in
df_concat = pd.concat([pred_df.iloc[:abs(len_diff)], hist_df[pair]], # append_model_predictions)
ignore_index=True, keys=hist_df[pair].keys()) new_pred.iloc[:, :] = np.nan
new_pred["date_pred"] = dataframe["date"]
hist_preds = self.historic_predictions[pair].copy()
# find the closest common date between new_pred and historic predictions
# and cut off the new_pred dataframe at that date
common_dates = pd.merge(new_pred, hist_preds, on="date_pred", how="inner")
if len(common_dates.index) > 0:
new_pred = new_pred.iloc[len(common_dates):]
else: else:
df_concat = hist_df[pair].tail(len(pred_df.index)).reset_index(drop=True) logger.warning("No common dates found between new predictions and historic "
"predictions. You likely left your FreqAI instance offline "
f"for more than {len(dataframe.index)} candles.")
df_concat = pd.concat([hist_preds, new_pred], ignore_index=True, keys=hist_preds.keys())
# remove last row because we will append that later in append_model_predictions()
df_concat = df_concat.iloc[:-1]
# any missing values will get zeroed out so users can see the exact
# downtime in FreqUI
df_concat = df_concat.fillna(0) df_concat = df_concat.fillna(0)
self.model_return_values[pair] = df_concat self.historic_predictions[pair] = df_concat
self.model_return_values[pair] = df_concat.tail(len(dataframe.index)).reset_index(drop=True)
def append_model_predictions(self, pair: str, predictions: DataFrame, def append_model_predictions(self, pair: str, predictions: DataFrame,
do_preds: NDArray[np.int_], do_preds: NDArray[np.int_],

View File

@@ -244,6 +244,14 @@ class FreqaiDataKitchen:
f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points" f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
f" due to NaNs in populated dataset {len(unfiltered_df)}." f" due to NaNs in populated dataset {len(unfiltered_df)}."
) )
if len(unfiltered_df) == 0 and not self.live:
raise OperationalException(
f"{self.pair}: all training data dropped due to NaNs. "
"You likely did not download enough training data prior "
"to your backtest timerange. Hint:\n"
"https://www.freqtrade.io/en/stable/freqai-running/"
"#downloading-data-to-cover-the-full-backtest-period"
)
if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live: if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live:
worst_indicator = str(unfiltered_df.count().idxmin()) worst_indicator = str(unfiltered_df.count().idxmin())
logger.warning( logger.warning(

View File

@@ -453,7 +453,7 @@ class IFreqaiModel(ABC):
pred_df, do_preds = self.predict(dataframe, dk) pred_df, do_preds = self.predict(dataframe, dk)
if pair not in self.dd.historic_predictions: if pair not in self.dd.historic_predictions:
self.set_initial_historic_predictions(pred_df, dk, pair, dataframe) self.set_initial_historic_predictions(pred_df, dk, pair, dataframe)
self.dd.set_initial_return_values(pair, pred_df) self.dd.set_initial_return_values(pair, pred_df, dataframe)
dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe) dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe)
return return
@@ -645,11 +645,11 @@ class IFreqaiModel(ABC):
If the user reuses an identifier on a subsequent instance, If the user reuses an identifier on a subsequent instance,
this function will not be called. In that case, "real" predictions this function will not be called. In that case, "real" predictions
will be appended to the loaded set of historic predictions. will be appended to the loaded set of historic predictions.
:param df: DataFrame = the dataframe containing the training feature data :param pred_df: DataFrame = the dataframe containing the predictions coming
:param model: Any = A model which was `fit` using a common library such as out of a model
catboost or lightgbm
:param dk: FreqaiDataKitchen = object containing methods for data analysis :param dk: FreqaiDataKitchen = object containing methods for data analysis
:param pair: str = current pair :param pair: str = current pair
:param strat_df: DataFrame = dataframe coming from strategy
""" """
self.dd.historic_predictions[pair] = pred_df self.dd.historic_predictions[pair] = pred_df

View File

@@ -456,7 +456,8 @@ class FreqtradeBot(LoggingMixin):
Only used balance disappeared, which would make exiting impossible. Only used balance disappeared, which would make exiting impossible.
""" """
try: try:
orders = self.exchange.fetch_orders(trade.pair, trade.open_date_utc) orders = self.exchange.fetch_orders(
trade.pair, trade.open_date_utc - timedelta(seconds=10))
prev_exit_reason = trade.exit_reason prev_exit_reason = trade.exit_reason
prev_trade_state = trade.is_open prev_trade_state = trade.is_open
for order in orders: for order in orders:
@@ -937,7 +938,8 @@ class FreqtradeBot(LoggingMixin):
# Don't call custom_entry_price in order-adjust scenario # Don't call custom_entry_price in order-adjust scenario
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=enter_limit_requested)( default_retval=enter_limit_requested)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, trade=trade,
current_time=datetime.now(timezone.utc),
proposed_rate=enter_limit_requested, entry_tag=entry_tag, proposed_rate=enter_limit_requested, entry_tag=entry_tag,
side=trade_side, side=trade_side,
) )
@@ -1362,18 +1364,21 @@ class FreqtradeBot(LoggingMixin):
self.handle_cancel_enter(trade, order, order_id, reason) self.handle_cancel_enter(trade, order, order_id, reason)
else: else:
canceled = self.handle_cancel_exit(trade, order, order_id, reason) canceled = self.handle_cancel_exit(trade, order, order_id, reason)
canceled_count = trade.get_exit_order_count() canceled_count = trade.get_canceled_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)
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: if (canceled and max_timeouts > 0 and canceled_count >= max_timeouts):
logger.warning(f'Emergency exiting trade {trade}, as the exit order ' logger.warning(f"Emergency exiting trade {trade}, as the exit order "
f'timed out {max_timeouts} times.') f"timed out {max_timeouts} times. force selling {order['amount']}.")
self.emergency_exit(trade, order['price']) self.emergency_exit(trade, order['price'], order['amount'])
def emergency_exit(self, trade: Trade, price: float) -> None: def emergency_exit(
self, trade: Trade, price: float, sub_trade_amt: Optional[float] = None) -> None:
try: try:
self.execute_trade_exit( self.execute_trade_exit(
trade, price, trade, price,
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT)) exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT),
sub_trade_amt=sub_trade_amt
)
except DependencyException as exception: except DependencyException as exception:
logger.warning( logger.warning(
f'Unable to emergency exit trade {trade.pair}: {exception}') f'Unable to emergency exit trade {trade.pair}: {exception}')
@@ -1848,7 +1853,7 @@ class FreqtradeBot(LoggingMixin):
def update_trade_state( def update_trade_state(
self, trade: Trade, order_id: Optional[str], self, trade: Trade, order_id: Optional[str],
action_order: Optional[Dict[str, Any]] = None, action_order: Optional[Dict[str, Any]] = None, *,
stoploss_order: bool = False, send_msg: bool = True) -> bool: stoploss_order: bool = False, send_msg: bool = True) -> bool:
""" """
Checks trades with open orders and updates the amount if necessary Checks trades with open orders and updates the amount if necessary
@@ -1885,7 +1890,7 @@ class FreqtradeBot(LoggingMixin):
self.handle_order_fee(trade, order_obj, order) self.handle_order_fee(trade, order_obj, order)
trade.update_trade(order_obj) trade.update_trade(order_obj, not send_msg)
trade = self._update_trade_after_fill(trade, order_obj) trade = self._update_trade_after_fill(trade, order_obj)
Trade.commit() Trade.commit()

View File

@@ -11,8 +11,8 @@ from freqtrade.util.gc_setup import gc_set_threshold
# check min. python version # check min. python version
if sys.version_info < (3, 8): # pragma: no cover if sys.version_info < (3, 9): # pragma: no cover
sys.exit("Freqtrade requires Python version >= 3.8") sys.exit("Freqtrade requires Python version >= 3.9")
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.commands import Arguments from freqtrade.commands import Arguments

View File

@@ -738,7 +738,9 @@ class Backtesting:
if order_type == 'limit': if order_type == 'limit':
new_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, new_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=propose_rate)( default_retval=propose_rate)(
pair=pair, current_time=current_time, pair=pair,
trade=trade, # type: ignore[arg-type]
current_time=current_time,
proposed_rate=propose_rate, entry_tag=entry_tag, proposed_rate=propose_rate, entry_tag=entry_tag,
side=direction, side=direction,
) # default value is the open rate ) # default value is the open rate

View File

@@ -13,8 +13,9 @@ from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select,
from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship, validates from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship, validates
from typing_extensions import Self from typing_extensions import Self
from freqtrade.constants import (CUSTOM_TAG_MAX_LENGTH, DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, from freqtrade.constants import (CANCELED_EXCHANGE_STATES, CUSTOM_TAG_MAX_LENGTH,
NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES,
BuySell, LongShort)
from freqtrade.enums import ExitType, TradingMode from freqtrade.enums import ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision, from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
@@ -627,8 +628,9 @@ class LocalTrade:
'amount_precision': self.amount_precision, 'amount_precision': self.amount_precision,
'price_precision': self.price_precision, 'price_precision': self.price_precision,
'precision_mode': self.precision_mode, 'precision_mode': self.precision_mode,
'orders': orders_json, 'contract_size': self.contract_size,
'has_open_orders': self.has_open_orders, 'has_open_orders': self.has_open_orders,
'orders': orders_json,
} }
@staticmethod @staticmethod
@@ -726,7 +728,7 @@ class LocalTrade:
f"Trailing stoploss saved us: " f"Trailing stoploss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss or 0.0):.8f}.") f"{float(self.stop_loss) - float(self.initial_stop_loss or 0.0):.8f}.")
def update_trade(self, order: Order) -> None: def update_trade(self, order: Order, recalculating: bool = False) -> None:
""" """
Updates this entity with amount and actual open/close rates. Updates this entity with amount and actual open/close rates.
:param order: order retrieved by exchange.fetch_order() :param order: order retrieved by exchange.fetch_order()
@@ -768,8 +770,9 @@ class LocalTrade:
self.precision_mode, self.contract_size) self.precision_mode, self.contract_size)
if ( if (
isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC) isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC)
or order.safe_amount_after_fee > amount_tr or (not recalculating and order.safe_amount_after_fee > amount_tr)
): ):
# When recalculating a trade, only comming out to 0 can force a close
self.close(order.safe_price) self.close(order.safe_price)
else: else:
self.recalc_trade_from_orders() self.recalc_trade_from_orders()
@@ -822,12 +825,13 @@ class LocalTrade:
def update_order(self, order: Dict) -> None: def update_order(self, order: Dict) -> None:
Order.update_orders(self.orders, order) Order.update_orders(self.orders, order)
def get_exit_order_count(self) -> int: def get_canceled_exit_order_count(self) -> int:
""" """
Get amount of failed exiting orders Get amount of failed exiting orders
assumes full exits. assumes full exits.
""" """
return len([o for o in self.orders if o.ft_order_side == self.exit_side]) return len([o for o in self.orders if o.ft_order_side == self.exit_side
and o.status in CANCELED_EXCHANGE_STATES])
def _calc_open_trade_value(self, amount: float, open_rate: float) -> float: def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
""" """
@@ -1786,6 +1790,10 @@ class Trade(ModelBase, LocalTrade):
is_short=data["is_short"], is_short=data["is_short"],
trading_mode=data["trading_mode"], trading_mode=data["trading_mode"],
funding_fees=data["funding_fees"], funding_fees=data["funding_fees"],
amount_precision=data.get('amount_precision', None),
price_precision=data.get('price_precision', None),
precision_mode=data.get('precision_mode', None),
contract_size=data.get('contract_size', None),
) )
for order in data["orders"]: for order in data["orders"]:

View File

@@ -30,7 +30,7 @@ class RangeStabilityFilter(IPairList):
self._days = pairlistconfig.get('lookback_days', 10) self._days = pairlistconfig.get('lookback_days', 10)
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01) self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change') self._max_rate_of_change = pairlistconfig.get('max_rate_of_change')
self._refresh_period = pairlistconfig.get('refresh_period', 1440) self._refresh_period = pairlistconfig.get('refresh_period', 86400)
self._def_candletype = self._config['candle_type_def'] self._def_candletype = self._config['candle_type_def']
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)

View File

@@ -395,7 +395,8 @@ 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, trade: Optional[Trade],
current_time: datetime, proposed_rate: float,
entry_tag: Optional[str], side: str, **kwargs) -> float: entry_tag: Optional[str], side: str, **kwargs) -> float:
""" """
Custom entry price logic, returning the new entry price. Custom entry price logic, returning the new entry price.
@@ -405,6 +406,7 @@ class IStrategy(ABC, HyperStrategyMixin):
When not implemented by a strategy, returns None, orderbook is used to set entry price When not implemented by a strategy, returns None, orderbook is used to set entry price
:param pair: Pair that's currently analyzed :param pair: Pair that's currently analyzed
:param trade: trade object (None for initial entries).
: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 exit_pricing. :param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
@@ -513,7 +515,7 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be Custom trade adjustment logic, returning the stake amount that a trade should be
increased or decreased. increased or decreased.
This means extra buy or sell orders with additional fees. This means extra entry or exit orders with additional fees.
Only called when `position_adjustment_enable` is set to True. 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/
@@ -522,8 +524,9 @@ class IStrategy(ABC, HyperStrategyMixin):
:param trade: trade object. :param trade: trade object.
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate. :param current_rate: Current entry rate (same as current_entry_profit)
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate
(same as current_entry_profit).
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits) :param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits). :param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
:param current_entry_rate: Current rate using entry pricing. :param current_entry_rate: Current rate using entry pricing.

View File

@@ -31,7 +31,7 @@ class FreqaiExampleStrategy(IStrategy):
plot_config = { plot_config = {
"main_plot": {}, "main_plot": {},
"subplots": { "subplots": {
"&-s_close": {"prediction": {"color": "blue"}}, "&-s_close": {"&-s_close": {"color": "blue"}},
"do_predict": { "do_predict": {
"do_predict": {"color": "brown"}, "do_predict": {"color": "brown"},
}, },

View File

@@ -13,7 +13,8 @@ def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
""" """
pass pass
def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: float, def custom_entry_price(self, pair: str, trade: Optional['Trade'],
current_time: 'datetime', proposed_rate: float,
entry_tag: 'Optional[str]', side: str, **kwargs) -> float: entry_tag: 'Optional[str]', side: str, **kwargs) -> float:
""" """
Custom entry price logic, returning the new entry price. Custom entry price logic, returning the new entry price.
@@ -23,6 +24,7 @@ def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate:
When not implemented by a strategy, returns None, orderbook is used to set entry price When not implemented by a strategy, returns None, orderbook is used to set entry price
:param pair: Pair that's currently analyzed :param pair: Pair that's currently analyzed
:param trade: trade object (None for initial entries).
: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 exit_pricing. :param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
@@ -257,7 +259,7 @@ def adjust_trade_position(self, trade: 'Trade', current_time: datetime,
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be Custom trade adjustment logic, returning the stake amount that a trade should be
increased or decreased. increased or decreased.
This means extra buy or sell orders with additional fees. This means extra entry or exit orders with additional fees.
Only called when `position_adjustment_enable` is set to True. 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/
@@ -266,8 +268,9 @@ def adjust_trade_position(self, trade: 'Trade', current_time: datetime,
:param trade: trade object. :param trade: trade object.
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate. :param current_rate: Current entry rate (same as current_entry_profit)
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate
(same as current_entry_profit).
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits) :param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits). :param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
:param current_entry_rate: Current rate using entry pricing. :param current_entry_rate: Current rate using entry pricing.
@@ -276,8 +279,8 @@ def adjust_trade_position(self, trade: 'Trade', current_time: datetime,
:param current_exit_profit: Current profit using exit pricing. :param current_exit_profit: Current profit using exit pricing.
: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: Stake amount to adjust your trade, :return float: Stake amount to adjust your trade,
Positive values to increase position, Negative values to decrease position. Positive values to increase position, Negative values to decrease position.
Return None for no action. Return None for no action.
""" """
return None return None

View File

@@ -65,6 +65,7 @@ ignore = ["freqtrade/vendor/**"]
line-length = 100 line-length = 100
extend-exclude = [".env", ".venv"] extend-exclude = [".env", ".venv"]
target-version = "py38" target-version = "py38"
# Exclude UP036 as it's causing the "exit if < 3.9" to fail.
extend-select = [ extend-select = [
"C90", # mccabe "C90", # mccabe
# "N", # pep8-naming # "N", # pep8-naming

View File

@@ -7,7 +7,7 @@
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==3.3.1 coveralls==3.3.1
ruff==0.0.287 ruff==0.0.290
mypy==1.5.1 mypy==1.5.1
pre-commit==3.4.0 pre-commit==3.4.0
pytest==7.4.2 pytest==7.4.2

View File

@@ -6,7 +6,7 @@
scikit-learn==1.1.3 scikit-learn==1.1.3
joblib==1.3.2 joblib==1.3.2
catboost==1.2.1; 'arm' not in platform_machine catboost==1.2.1; 'arm' not in platform_machine
lightgbm==4.0.0 lightgbm==4.1.0
xgboost==1.7.6 xgboost==2.0.0
tensorboard==2.14.0 tensorboard==2.14.0
datasieve==0.1.7 datasieve==0.1.7

View File

@@ -5,4 +5,4 @@
scipy==1.11.2 scipy==1.11.2
scikit-learn==1.1.3 scikit-learn==1.1.3
scikit-optimize==0.9.0 scikit-optimize==0.9.0
filelock==3.12.3 filelock==3.12.4

View File

@@ -1,4 +1,4 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==5.16.1 plotly==5.17.0

View File

@@ -1,4 +1,4 @@
numpy==1.25.2 numpy==1.26.0
pandas==2.0.3 pandas==2.0.3
pandas-ta==0.3.14b pandas-ta==0.3.14b
@@ -23,14 +23,14 @@ jinja2==3.1.2
tables==3.8.0 tables==3.8.0
blosc==1.11.1 blosc==1.11.1
joblib==1.3.2 joblib==1.3.2
rich==13.5.2 rich==13.5.3
pyarrow==13.0.0; platform_machine != 'armv7l' pyarrow==13.0.0; platform_machine != 'armv7l'
# find first, C search in arrays # find first, C search in arrays
py_find_1st==1.1.5 py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.10 python-rapidjson==1.11
# Properly format api responses # Properly format api responses
orjson==3.9.7 orjson==3.9.7

View File

@@ -24,7 +24,7 @@ from tests.conftest import (EXMS, generate_test_data_raw, get_mock_coro, get_pat
# Make sure to always keep one exchange here which is NOT subclassed!! # Make sure to always keep one exchange here which is NOT subclassed!!
EXCHANGES = ['bittrex', 'binance', 'kraken', 'gate', 'kucoin', 'bybit'] EXCHANGES = ['bittrex', 'binance', 'kraken', 'gate', 'kucoin', 'bybit', 'okx']
get_entry_rate_data = [ get_entry_rate_data = [
('other', 20, 19, 10, 0.0, 20), # Full ask side ('other', 20, 19, 10, 0.0, 20), # Full ask side
@@ -1312,8 +1312,11 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
leverage=3.0 leverage=3.0
) )
assert exchange._set_leverage.call_count == 1 if exchange_name != 'okx':
assert exchange.set_margin_mode.call_count == 1 assert exchange._set_leverage.call_count == 1
assert exchange.set_margin_mode.call_count == 1
else:
assert api_mock.set_leverage.call_count == 1
assert order['amount'] == 0.01 assert order['amount'] == 0.01
@@ -1677,7 +1680,10 @@ def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
api_mock.fetch_closed_orders = MagicMock(return_value=[limit_order['buy']]) api_mock.fetch_closed_orders = MagicMock(return_value=[limit_order['buy']])
mocker.patch(f'{EXMS}.exchange_has', return_value=True) mocker.patch(f'{EXMS}.exchange_has', return_value=True)
start_time = datetime.now(timezone.utc) - timedelta(days=5) start_time = datetime.now(timezone.utc) - timedelta(days=20)
expected = 1
if exchange_name == 'bybit':
expected = 3
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
# Not available in dry-run # Not available in dry-run
@@ -1687,10 +1693,10 @@ def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
res = exchange.fetch_orders('mocked', start_time) res = exchange.fetch_orders('mocked', start_time)
assert api_mock.fetch_orders.call_count == 1 assert api_mock.fetch_orders.call_count == expected
assert api_mock.fetch_open_orders.call_count == 0 assert api_mock.fetch_open_orders.call_count == 0
assert api_mock.fetch_closed_orders.call_count == 0 assert api_mock.fetch_closed_orders.call_count == 0
assert len(res) == 2 assert len(res) == 2 * expected
res = exchange.fetch_orders('mocked', start_time) res = exchange.fetch_orders('mocked', start_time)
@@ -1704,13 +1710,17 @@ def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
if endpoint == 'fetchOpenOrders': if endpoint == 'fetchOpenOrders':
return True return True
if exchange_name == 'okx':
# Special OKX case is tested separately
return
mocker.patch(f'{EXMS}.exchange_has', has_resp) mocker.patch(f'{EXMS}.exchange_has', has_resp)
# happy path without fetchOrders # happy path without fetchOrders
res = exchange.fetch_orders('mocked', start_time) exchange.fetch_orders('mocked', start_time)
assert api_mock.fetch_orders.call_count == 0 assert api_mock.fetch_orders.call_count == 0
assert api_mock.fetch_open_orders.call_count == 1 assert api_mock.fetch_open_orders.call_count == expected
assert api_mock.fetch_closed_orders.call_count == 1 assert api_mock.fetch_closed_orders.call_count == expected
mocker.patch(f'{EXMS}.exchange_has', return_value=True) mocker.patch(f'{EXMS}.exchange_has', return_value=True)
@@ -1723,11 +1733,11 @@ def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
api_mock.fetch_open_orders.reset_mock() api_mock.fetch_open_orders.reset_mock()
api_mock.fetch_closed_orders.reset_mock() api_mock.fetch_closed_orders.reset_mock()
res = exchange.fetch_orders('mocked', start_time) exchange.fetch_orders('mocked', start_time)
assert api_mock.fetch_orders.call_count == 1 assert api_mock.fetch_orders.call_count == expected
assert api_mock.fetch_open_orders.call_count == 1 assert api_mock.fetch_open_orders.call_count == expected
assert api_mock.fetch_closed_orders.call_count == 1 assert api_mock.fetch_closed_orders.call_count == expected
def test_fetch_trading_fees(default_conf, mocker): def test_fetch_trading_fees(default_conf, mocker):
@@ -2044,7 +2054,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
) )
# Required candles # Required candles
candles = (end_ts - start_ts) / 300_000 candles = (end_ts - start_ts) / 300_000
exp = candles // exchange.ohlcv_candle_limit('5m', CandleType.SPOT) + 1 exp = candles // exchange.ohlcv_candle_limit('5m', candle_type, start_ts) + 1
# Depending on the exchange, this should be called between 1 and 6 times. # Depending on the exchange, this should be called between 1 and 6 times.
assert exchange._api_async.fetch_ohlcv.call_count == exp assert exchange._api_async.fetch_ohlcv.call_count == exp
@@ -3122,25 +3132,28 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
default_conf['dry_run'] = False default_conf['dry_run'] = False
mock_prefix = 'freqtrade.exchange.gate.Gate'
if exchange_name == 'okx':
mock_prefix = 'freqtrade.exchange.okx.Okx'
mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value={'for': 123}) mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value={'for': 123})
mocker.patch('freqtrade.exchange.gate.Gate.fetch_stoploss_order', return_value={'for': 123}) mocker.patch(f'{mock_prefix}.fetch_stoploss_order', return_value={'for': 123})
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
res = {'fee': {}, 'status': 'canceled', 'amount': 1234} res = {'fee': {}, 'status': 'canceled', 'amount': 1234}
mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=res) mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=res)
mocker.patch('freqtrade.exchange.gate.Gate.cancel_stoploss_order', return_value=res) mocker.patch(f'{mock_prefix}.cancel_stoploss_order', return_value=res)
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co == res assert co == res
mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value='canceled') mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value='canceled')
mocker.patch('freqtrade.exchange.gate.Gate.cancel_stoploss_order', return_value='canceled') mocker.patch(f'{mock_prefix}.cancel_stoploss_order', return_value='canceled')
# Fall back to fetch_stoploss_order # Fall back to fetch_stoploss_order
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co == {'for': 123} assert co == {'for': 123}
exc = InvalidOrderException("") exc = InvalidOrderException("")
mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=exc) mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=exc)
mocker.patch('freqtrade.exchange.gate.Gate.fetch_stoploss_order', side_effect=exc) mocker.patch(f'{mock_prefix}.fetch_stoploss_order', side_effect=exc)
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co['amount'] == 555 assert co['amount'] == 555
assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}} assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}}
@@ -3148,7 +3161,7 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
exc = InvalidOrderException("Did not find order") exc = InvalidOrderException("Did not find order")
mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=exc) mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=exc)
mocker.patch('freqtrade.exchange.gate.Gate.cancel_stoploss_order', side_effect=exc) mocker.patch(f'{mock_prefix}.cancel_stoploss_order', side_effect=exc)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123) exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123)
@@ -3223,8 +3236,14 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.fetch_order = MagicMock(return_value={'id': '123', 'symbol': 'TKN/BTC'}) api_mock.fetch_order = MagicMock(return_value={'id': '123', 'symbol': 'TKN/BTC'})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == {'id': '123', 'symbol': 'TKN/BTC'} res = {'id': '123', 'symbol': 'TKN/BTC'}
if exchange_name == 'okx':
res = {'id': '123', 'symbol': 'TKN/BTC', 'type': 'stoploss'}
assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == res
if exchange_name == 'okx':
# Tested separately.
return
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
@@ -3544,6 +3563,8 @@ def test_get_markets_error(default_conf, mocker):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_ohlcv_candle_limit(default_conf, mocker, exchange_name): def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
if exchange_name == 'okx':
pytest.skip("Tested separately for okx")
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
timeframes = ('1m', '5m', '1h') timeframes = ('1m', '5m', '1h')
expected = exchange._ft_has['ohlcv_candle_limit'] expected = exchange._ft_has['ohlcv_candle_limit']

View File

@@ -618,3 +618,70 @@ def test__get_stop_params_okx(mocker, default_conf):
assert params['tdMode'] == 'isolated' assert params['tdMode'] == 'isolated'
assert params['posSide'] == 'net' assert params['posSide'] == 'net'
def test_fetch_orders_okx(default_conf, mocker, limit_order):
api_mock = MagicMock()
api_mock.fetch_orders = MagicMock(return_value=[
limit_order['buy'],
limit_order['sell'],
])
api_mock.fetch_open_orders = MagicMock(return_value=[limit_order['buy']])
api_mock.fetch_closed_orders = MagicMock(return_value=[limit_order['buy']])
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
start_time = datetime.now(timezone.utc) - timedelta(days=20)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='okx')
# Not available in dry-run
assert exchange.fetch_orders('mocked', start_time) == []
assert api_mock.fetch_orders.call_count == 0
default_conf['dry_run'] = False
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='okx')
def has_resp(_, endpoint):
if endpoint == 'fetchOrders':
return False
if endpoint == 'fetchClosedOrders':
return True
if endpoint == 'fetchOpenOrders':
return True
mocker.patch(f'{EXMS}.exchange_has', has_resp)
history_params = {'method': 'privateGetTradeOrdersHistoryArchive'}
# happy path without fetchOrders
exchange.fetch_orders('mocked', start_time)
assert api_mock.fetch_orders.call_count == 0
assert api_mock.fetch_open_orders.call_count == 1
assert api_mock.fetch_closed_orders.call_count == 2
assert 'params' not in api_mock.fetch_closed_orders.call_args_list[0][1]
assert api_mock.fetch_closed_orders.call_args_list[1][1]['params'] == history_params
api_mock.fetch_open_orders.reset_mock()
api_mock.fetch_closed_orders.reset_mock()
# regular closed_orders endpoint only has history for 7 days.
exchange.fetch_orders('mocked', datetime.now(timezone.utc) - timedelta(days=6))
assert api_mock.fetch_orders.call_count == 0
assert api_mock.fetch_open_orders.call_count == 1
assert api_mock.fetch_closed_orders.call_count == 1
assert 'params' not in api_mock.fetch_closed_orders.call_args_list[0][1]
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
# Unhappy path - first fetch-orders call fails.
api_mock.fetch_orders = MagicMock(side_effect=ccxt.NotSupported())
api_mock.fetch_open_orders.reset_mock()
api_mock.fetch_closed_orders.reset_mock()
exchange.fetch_orders('mocked', start_time)
assert api_mock.fetch_orders.call_count == 1
assert api_mock.fetch_open_orders.call_count == 1
assert api_mock.fetch_closed_orders.call_count == 2
assert 'params' not in api_mock.fetch_closed_orders.call_args_list[0][1]
assert api_mock.fetch_closed_orders.call_args_list[1][1]['params'] == history_params

View File

@@ -1,7 +1,9 @@
import shutil import shutil
from pathlib import Path from pathlib import Path
from unittest.mock import patch
import pandas as pd
import pytest import pytest
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
@@ -135,3 +137,111 @@ def test_get_timerange_from_backtesting_live_df_pred_not_found(mocker, freqai_co
match=r'Historic predictions not found.*' match=r'Historic predictions not found.*'
): ):
freqai.dd.get_timerange_from_live_historic_predictions() freqai.dd.get_timerange_from_live_historic_predictions()
def test_set_initial_return_values(mocker, freqai_conf):
"""
Simple test of the set initial return values that ensures
we are concatening and ffilling values properly.
"""
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange)
freqai = strategy.freqai
freqai.live = False
freqai.dk = FreqaiDataKitchen(freqai_conf)
# Setup
pair = "BTC/USD"
end_x = "2023-08-31"
start_x_plus_1 = "2023-08-30"
end_x_plus_5 = "2023-09-03"
historic_data = {
'date_pred': pd.date_range(end=end_x, periods=5),
'value': range(1, 6)
}
new_data = {
'date': pd.date_range(start=start_x_plus_1, end=end_x_plus_5),
'value': range(6, 11)
}
freqai.dd.historic_predictions[pair] = pd.DataFrame(historic_data)
new_pred_df = pd.DataFrame(new_data)
dataframe = pd.DataFrame(new_data)
# Action
with patch('logging.Logger.warning') as mock_logger_warning:
freqai.dd.set_initial_return_values(pair, new_pred_df, dataframe)
# Assertions
hist_pred_df = freqai.dd.historic_predictions[pair]
model_return_df = freqai.dd.model_return_values[pair]
assert (hist_pred_df['date_pred'].iloc[-1] ==
pd.Timestamp(end_x_plus_5) - pd.Timedelta(days=1))
assert 'date_pred' in hist_pred_df.columns
assert hist_pred_df.shape[0] == 7 # Total rows: 5 from historic and 2 new zeros
# compare values in model_return_df with hist_pred_df
assert (model_return_df["value"].values ==
hist_pred_df.tail(len(dataframe))["value"].values).all()
assert model_return_df.shape[0] == len(dataframe)
# Ensure logger error is not called
mock_logger_warning.assert_not_called()
def test_set_initial_return_values_warning(mocker, freqai_conf):
"""
Simple test of set_initial_return_values that hits the warning
associated with leaving a FreqAI bot offline so long that the
exchange candles have no common date with the historic predictions
"""
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange)
freqai = strategy.freqai
freqai.live = False
freqai.dk = FreqaiDataKitchen(freqai_conf)
# Setup
pair = "BTC/USD"
end_x = "2023-08-31"
start_x_plus_1 = "2023-09-01"
end_x_plus_5 = "2023-09-05"
historic_data = {
'date_pred': pd.date_range(end=end_x, periods=5),
'value': range(1, 6)
}
new_data = {
'date': pd.date_range(start=start_x_plus_1, end=end_x_plus_5),
'value': range(6, 11)
}
freqai.dd.historic_predictions[pair] = pd.DataFrame(historic_data)
new_pred_df = pd.DataFrame(new_data)
dataframe = pd.DataFrame(new_data)
# Action
with patch('logging.Logger.warning') as mock_logger_warning:
freqai.dd.set_initial_return_values(pair, new_pred_df, dataframe)
# Assertions
hist_pred_df = freqai.dd.historic_predictions[pair]
model_return_df = freqai.dd.model_return_values[pair]
assert hist_pred_df['date_pred'].iloc[-1] == pd.Timestamp(end_x_plus_5) - pd.Timedelta(days=1)
assert 'date_pred' in hist_pred_df.columns
assert hist_pred_df.shape[0] == 9 # Total rows: 5 from historic and 4 new zeros
# compare values in model_return_df with hist_pred_df
assert (model_return_df["value"].values == hist_pred_df.tail(
len(dataframe))["value"].values).all()
assert model_return_df.shape[0] == len(dataframe)
# Ensure logger error is not called
mock_logger_warning.assert_called()

View File

@@ -1385,6 +1385,7 @@ def test_to_json(fee):
precision_mode=1, precision_mode=1,
amount_precision=8.0, amount_precision=8.0,
price_precision=7.0, price_precision=7.0,
contract_size=1,
) )
result = trade.to_json() result = trade.to_json()
assert isinstance(result, dict) assert isinstance(result, dict)
@@ -1450,6 +1451,7 @@ def test_to_json(fee):
'amount_precision': 8.0, 'amount_precision': 8.0,
'price_precision': 7.0, 'price_precision': 7.0,
'precision_mode': 1, 'precision_mode': 1,
'contract_size': 1,
'orders': [], 'orders': [],
'has_open_orders': False, 'has_open_orders': False,
} }
@@ -1471,6 +1473,7 @@ def test_to_json(fee):
precision_mode=2, precision_mode=2,
amount_precision=7.0, amount_precision=7.0,
price_precision=8.0, price_precision=8.0,
contract_size=1
) )
result = trade.to_json() result = trade.to_json()
assert isinstance(result, dict) assert isinstance(result, dict)
@@ -1536,6 +1539,7 @@ def test_to_json(fee):
'amount_precision': 7.0, 'amount_precision': 7.0,
'price_precision': 8.0, 'price_precision': 8.0,
'precision_mode': 2, 'precision_mode': 2,
'contract_size': 1,
'orders': [], 'orders': [],
'has_open_orders': False, 'has_open_orders': False,
} }
@@ -1928,11 +1932,15 @@ def test_get_best_pair_lev(fee):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('is_short', [True, False]) @pytest.mark.parametrize('is_short', [True, False])
def test_get_exit_order_count(fee, is_short): def test_get_canceled_exit_order_count(fee, is_short):
create_mock_trades(fee, is_short=is_short) create_mock_trades(fee, is_short=is_short)
trade = Trade.get_trades([Trade.pair == 'ETC/BTC']).first() trade = Trade.get_trades([Trade.pair == 'ETC/BTC']).first()
assert trade.get_exit_order_count() == 1 # No canceled order.
assert trade.get_canceled_exit_order_count() == 0
trade.orders[-1].status = 'canceled'
assert trade.get_canceled_exit_order_count() == 1
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")

View File

@@ -66,6 +66,10 @@ def test_trade_fromjson():
"is_short": false, "is_short": false,
"trading_mode": "spot", "trading_mode": "spot",
"funding_fees": 0.0, "funding_fees": 0.0,
"amount_precision": 1.0,
"price_precision": 3.0,
"precision_mode": 2,
"contract_size": 1.0,
"open_order_id": null, "open_order_id": null,
"orders": [ "orders": [
{ {
@@ -180,6 +184,9 @@ def test_trade_fromjson():
assert isinstance(trade.open_date, datetime) assert isinstance(trade.open_date, datetime)
assert trade.exit_reason == 'no longer good' assert trade.exit_reason == 'no longer good'
assert trade.realized_profit == 2.76315361 assert trade.realized_profit == 2.76315361
assert trade.precision_mode == 2
assert trade.amount_precision == 1.0
assert trade.contract_size == 1.0
assert len(trade.orders) == 5 assert len(trade.orders) == 5
last_o = trade.orders[-1] last_o = trade.orders[-1]

View File

@@ -90,6 +90,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'amount_precision': 8.0, 'amount_precision': 8.0,
'price_precision': 8.0, 'price_precision': 8.0,
'precision_mode': 2, 'precision_mode': 2,
'contract_size': 1,
'has_open_orders': False, 'has_open_orders': False,
'orders': [{ 'orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05, 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
@@ -263,7 +264,11 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert isnan(fiat_profit_sum) assert isnan(fiat_profit_sum)
def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee, markets, mocker) -> None: def test__rpc_timeunit_profit(
default_conf_usdt, ticker, fee, markets, mocker, time_machine) -> None:
time_machine.move_to("2023-09-05 10:00:00 +00:00", tick=False)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
EXMS, EXMS,

View File

@@ -6,6 +6,8 @@ from typing import Optional
from pandas import DataFrame from pandas import DataFrame
from strategy_test_v3 import StrategyTestV3 from strategy_test_v3 import StrategyTestV3
from freqtrade.persistence import Trade
class StrategyTestV3CustomEntryPrice(StrategyTestV3): class StrategyTestV3CustomEntryPrice(StrategyTestV3):
""" """
@@ -31,7 +33,8 @@ class StrategyTestV3CustomEntryPrice(StrategyTestV3):
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe return dataframe
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, def custom_entry_price(self, pair: str, trade: Optional[Trade], current_time: datetime,
proposed_rate: float,
entry_tag: Optional[str], side: str, **kwargs) -> float: entry_tag: Optional[str], side: str, **kwargs) -> float:
return self.new_entry_price return self.new_entry_price

View File

@@ -2991,6 +2991,8 @@ def test_manage_open_orders_exit_usercustom(
is_short, open_trade_usdt, caplog is_short, open_trade_usdt, caplog
) -> None: ) -> None:
default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1} default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1}
limit_sell_order_old['amount'] = open_trade_usdt.amount
limit_sell_order_old['remaining'] = open_trade_usdt.amount
if is_short: if is_short:
limit_sell_order_old['side'] = 'buy' limit_sell_order_old['side'] = 'buy'
@@ -3052,7 +3054,7 @@ def test_manage_open_orders_exit_usercustom(
# 2nd canceled trade - Fail execute exit # 2nd canceled trade - Fail execute exit
caplog.clear() caplog.clear()
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1) mocker.patch('freqtrade.persistence.Trade.get_canceled_exit_order_count', return_value=1)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit',
side_effect=DependencyException) side_effect=DependencyException)
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
@@ -5658,6 +5660,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_handle_onexchange_order(mocker, default_conf_usdt, limit_order, is_short, caplog): def test_handle_onexchange_order(mocker, default_conf_usdt, limit_order, is_short, caplog):
default_conf_usdt['dry_run'] = False
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mock_uts = mocker.spy(freqtrade, 'update_trade_state') mock_uts = mocker.spy(freqtrade, 'update_trade_state')
@@ -5669,17 +5672,17 @@ def test_handle_onexchange_order(mocker, default_conf_usdt, limit_order, is_shor
]) ])
trade = Trade( trade = Trade(
pair='ETH/USDT', pair='ETH/USDT',
fee_open=0.001, fee_open=0.001,
fee_close=0.001, fee_close=0.001,
open_rate=entry_order['price'], open_rate=entry_order['price'],
open_date=dt_now(), open_date=dt_now(),
stake_amount=entry_order['cost'], stake_amount=entry_order['cost'],
amount=entry_order['amount'], amount=entry_order['amount'],
exchange="binance", exchange="binance",
is_short=is_short, is_short=is_short,
leverage=1, leverage=1,
) )
trade.orders.append(Order.parse_from_ccxt_object( trade.orders.append(Order.parse_from_ccxt_object(
entry_order, 'ADA/USDT', entry_side(is_short)) entry_order, 'ADA/USDT', entry_side(is_short))
@@ -5698,6 +5701,77 @@ def test_handle_onexchange_order(mocker, default_conf_usdt, limit_order, is_shor
assert trade.exit_reason == ExitType.SOLD_ON_EXCHANGE.value assert trade.exit_reason == ExitType.SOLD_ON_EXCHANGE.value
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [False, True])
def test_handle_onexchange_order_exit(mocker, default_conf_usdt, limit_order, is_short, caplog):
default_conf_usdt['dry_run'] = False
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mock_uts = mocker.spy(freqtrade, 'update_trade_state')
entry_order = limit_order[entry_side(is_short)]
add_entry_order = deepcopy(entry_order)
add_entry_order.update({
'id': '_partial_entry_id',
'amount': add_entry_order['amount'] / 1.5,
'cost': add_entry_order['cost'] / 1.5,
'filled': add_entry_order['filled'] / 1.5,
})
exit_order_part = deepcopy(limit_order[exit_side(is_short)])
exit_order_part.update({
'id': 'some_random_partial_id',
'amount': exit_order_part['amount'] / 2,
'cost': exit_order_part['cost'] / 2,
'filled': exit_order_part['filled'] / 2,
})
exit_order = limit_order[exit_side(is_short)]
# Orders intentionally in the wrong sequence
mock_fo = mocker.patch(f'{EXMS}.fetch_orders', return_value=[
entry_order,
exit_order_part,
exit_order,
add_entry_order,
])
trade = Trade(
pair='ETH/USDT',
fee_open=0.001,
fee_close=0.001,
open_rate=entry_order['price'],
open_date=dt_now(),
stake_amount=entry_order['cost'],
amount=entry_order['amount'],
exchange="binance",
is_short=is_short,
leverage=1,
is_open=True,
)
trade.orders = [
Order.parse_from_ccxt_object(entry_order, trade.pair, entry_side(is_short)),
Order.parse_from_ccxt_object(exit_order_part, trade.pair, exit_side(is_short)),
Order.parse_from_ccxt_object(add_entry_order, trade.pair, entry_side(is_short)),
Order.parse_from_ccxt_object(exit_order, trade.pair, exit_side(is_short)),
]
trade.recalc_trade_from_orders()
Trade.session.add(trade)
Trade.commit()
freqtrade.handle_onexchange_order(trade)
# assert log_has_re(r"Found previously unknown order .*", caplog)
# Update trade state is called three times, once for every order
assert mock_uts.call_count == 4
assert mock_fo.call_count == 1
trade = Trade.session.scalars(select(Trade)).first()
assert len(trade.orders) == 4
assert trade.is_open is True
assert trade.exit_reason is None
assert trade.amount == 5.0
def test_get_valid_price(mocker, default_conf_usdt) -> None: def test_get_valid_price(mocker, default_conf_usdt) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)