mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-02-20 03:11:38 +00:00
Merge branch 'freqtrade:develop' into bt-metrics2
This commit is contained in:
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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_],
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]:
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user