diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index f8be8f66f..2572c03f1 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = 'develop' +__version__ = '2022.8.dev' if 'dev' in __version__: try: diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index cf41c0ca9..730a4e47f 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -4,5 +4,4 @@ from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.configuration import Configuration -from freqtrade.configuration.PeriodicCache import PeriodicCache from freqtrade.configuration.timerange import TimeRange diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index e21f10193..21cead77f 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -12,12 +12,12 @@ from typing import Any, Dict, List, Optional, Tuple from pandas import DataFrame from freqtrade.configuration import TimeRange -from freqtrade.configuration.PeriodicCache import PeriodicCache from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe from freqtrade.data.history import load_pair_history from freqtrade.enums import CandleType, RunMode from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exchange import Exchange, timeframe_to_seconds +from freqtrade.util import PeriodicCache logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b6996211f..18598e92d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -16,7 +16,7 @@ import arrow import ccxt import ccxt.async_support as ccxt_async from cachetools import TTLCache -from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, Precise, decimal_to_precision +from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision from pandas import DataFrame from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell, @@ -32,6 +32,7 @@ from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGE retrier_async) from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist +from freqtrade.util import FtPrecise CcxtModuleType = Any @@ -708,10 +709,10 @@ class Exchange: # counting_mode=self.precisionMode, # )) if self.precisionMode == TICK_SIZE: - precision = Precise(str(self.markets[pair]['precision']['price'])) - price_str = Precise(str(price)) + precision = FtPrecise(self.markets[pair]['precision']['price']) + price_str = FtPrecise(price) missing = price_str % precision - if not missing == Precise("0"): + if not missing == FtPrecise("0"): price = round(float(str(price_str - missing + precision)), 14) else: symbol_prec = self.markets[pair]['precision']['price'] @@ -849,6 +850,7 @@ class Exchange: dry_order.update({ 'average': average, 'filled': _amount, + 'remaining': 0.0, 'cost': (dry_order['amount'] * average) / leverage }) # market orders will always incurr taker fees diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 9ee6894f1..b3c219542 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,6 +1,6 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple import ccxt @@ -116,9 +116,17 @@ class Ftx(Exchange): if len(order) == 1: if order[0].get('status') == 'closed': # Trigger order was triggered ... - real_order_id = order[0].get('info', {}).get('orderId') + real_order_id: Optional[str] = order[0].get('info', {}).get('orderId') # OrderId may be None for stoploss-market orders - # But contains "average" in these cases. + # So we need to get it through the endpoint + # /conditional_orders/{conditional_order_id}/triggers + if not real_order_id: + res = self._api.privateGetConditionalOrdersConditionalOrderIdTriggers( + params={'conditional_order_id': order_id}) + self._log_exchange_response('fetch_stoploss_order2', res) + real_order_id = res['result'][0]['orderId'] if res.get( + 'result', []) else None + if real_order_id: order1 = self._api.fetch_order(real_order_id, pair) self._log_exchange_response('fetch_stoploss_order1', order1) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 390c8e8f6..0dbeb2e44 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -624,7 +624,8 @@ class FreqtradeBot(LoggingMixin): ordertype: Optional[str] = None, enter_tag: Optional[str] = None, trade: Optional[Trade] = None, - order_adjust: bool = False + order_adjust: bool = False, + leverage_: Optional[float] = None, ) -> bool: """ Executes a limit buy for the given pair @@ -640,7 +641,7 @@ class FreqtradeBot(LoggingMixin): pos_adjust = trade is not None enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( - pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust) + pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_) if not stake_amount: return False @@ -787,6 +788,7 @@ class FreqtradeBot(LoggingMixin): entry_tag: Optional[str], trade: Optional[Trade], order_adjust: bool, + leverage_: Optional[float], ) -> Tuple[float, float, float]: if price: @@ -809,16 +811,19 @@ class FreqtradeBot(LoggingMixin): if not enter_limit_requested: raise PricingError('Could not determine entry price.') - if trade is None: + if self.trading_mode != TradingMode.SPOT and trade is None: max_leverage = self.exchange.get_max_leverage(pair, stake_amount) - leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( - pair=pair, - current_time=datetime.now(timezone.utc), - current_rate=enter_limit_requested, - proposed_leverage=1.0, - max_leverage=max_leverage, - side=trade_side, entry_tag=entry_tag, - ) if self.trading_mode != TradingMode.SPOT else 1.0 + if leverage_: + leverage = leverage_ + else: + leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( + pair=pair, + current_time=datetime.now(timezone.utc), + current_rate=enter_limit_requested, + proposed_leverage=1.0, + max_leverage=max_leverage, + side=trade_side, entry_tag=entry_tag, + ) # Cap leverage between 1.0 and max_leverage. leverage = min(max(leverage, 1.0), max_leverage) else: diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index 786f32e88..13c992c87 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -8,11 +8,11 @@ from typing import Any, Dict, List, Optional import arrow from pandas import DataFrame -from freqtrade.configuration import PeriodicCache from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList +from freqtrade.util import PeriodicCache logger = logging.getLogger(__name__) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 333f2fe6e..ada20230a 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -194,11 +194,11 @@ class OrderSchema(BaseModel): pair: str order_id: str status: str - remaining: float + remaining: Optional[float] amount: float safe_price: float cost: float - filled: float + filled: Optional[float] ft_order_side: str order_type: str is_open: bool @@ -325,11 +325,13 @@ class ForceEnterPayload(BaseModel): ordertype: Optional[OrderTypeValues] stakeamount: Optional[float] entry_tag: Optional[str] + leverage: Optional[float] class ForceExitPayload(BaseModel): tradeid: str ordertype: Optional[OrderTypeValues] + amount: Optional[float] class BlacklistPayload(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index b3506409d..e0fef7be8 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -37,7 +37,8 @@ logger = logging.getLogger(__name__) # 2.14: Add entry/exit orders to trade response # 2.15: Add backtest history endpoints # 2.16: Additional daily metrics -API_VERSION = 2.16 +# 2.17: Forceentry - leverage, partial force_exit +API_VERSION = 2.17 # Public API, requires no auth. router_public = APIRouter() @@ -142,12 +143,11 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g @router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading']) def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None - stake_amount = payload.stakeamount if payload.stakeamount else None - entry_tag = payload.entry_tag if payload.entry_tag else 'force_entry' trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side, - order_type=ordertype, stake_amount=stake_amount, - enter_tag=entry_tag) + order_type=ordertype, stake_amount=payload.stakeamount, + enter_tag=payload.entry_tag or 'force_entry', + leverage=payload.leverage) if trade: return ForceEnterResponse.parse_obj(trade.to_json()) @@ -161,7 +161,7 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None - return rpc._rpc_force_exit(payload.tradeid, ordertype) + return rpc._rpc_force_exit(payload.tradeid, ordertype, amount=payload.amount) @router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d848da546..ed7f13a96 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -179,8 +179,10 @@ class RPC: else: current_rate = trade.close_rate if len(trade.select_filled_orders(trade.entry_side)) > 0: - current_profit = trade.calc_profit_ratio(current_rate) - current_profit_abs = trade.calc_profit(current_rate) + current_profit = trade.calc_profit_ratio( + current_rate) if not isnan(current_rate) else NAN + current_profit_abs = trade.calc_profit( + current_rate) if not isnan(current_rate) else NAN current_profit_fiat: Optional[float] = None # Calculate fiat profit if self._fiat_converter: @@ -239,12 +241,15 @@ class RPC: trade.pair, side='exit', is_short=trade.is_short, refresh=False) except (PricingError, ExchangeError): current_rate = NAN - if len(trade.select_filled_orders(trade.entry_side)) > 0: - trade_profit = trade.calc_profit(current_rate) - profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' + trade_profit = NAN + profit_str = f'{NAN:.2%}' else: - trade_profit = 0.0 - profit_str = f'{0.0:.2f}' + if trade.nr_of_successful_entries > 0: + trade_profit = trade.calc_profit(current_rate) + profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' + else: + trade_profit = 0.0 + profit_str = f'{0.0:.2f}' direction_str = ('S' if trade.is_short else 'L') if nonspot else '' if self._fiat_converter: fiat_profit = self._fiat_converter.convert_amount( @@ -424,8 +429,6 @@ class RPC: for trade in trades: current_rate: float = 0.0 - if not trade.open_rate: - continue if trade.close_date: durations.append((trade.close_date - trade.open_date).total_seconds()) @@ -447,9 +450,13 @@ class RPC: trade.pair, side='exit', is_short=trade.is_short, refresh=False) except (PricingError, ExchangeError): current_rate = NAN - profit_ratio = trade.calc_profit_ratio(rate=current_rate) - profit_abs = trade.calc_profit( - rate=trade.close_rate or current_rate) + trade.realized_profit + if isnan(current_rate): + profit_ratio = NAN + profit_abs = NAN + else: + profit_ratio = trade.calc_profit_ratio(rate=current_rate) + profit_abs = trade.calc_profit( + rate=trade.close_rate or current_rate) + trade.realized_profit profit_all_coin.append(profit_abs) profit_all_ratio.append(profit_ratio) @@ -660,36 +667,48 @@ class RPC: return {'status': 'No more buy will occur from now. Run /reload_config to reset.'} - def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]: + def __exec_force_exit(self, trade: Trade, ordertype: Optional[str], + amount: Optional[float] = None) -> None: + # Check if there is there is an open order + fully_canceled = False + if trade.open_order_id: + order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) + + if order['side'] == trade.entry_side: + fully_canceled = self._freqtrade.handle_cancel_enter( + trade, order, CANCEL_REASON['FORCE_EXIT']) + + if order['side'] == trade.exit_side: + # Cancel order - so it is placed anew with a fresh price. + self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT']) + + if not fully_canceled: + # Get current rate and execute sell + current_rate = self._freqtrade.exchange.get_rate( + trade.pair, side='exit', is_short=trade.is_short, refresh=True) + exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT) + order_type = ordertype or self._freqtrade.strategy.order_types.get( + "force_exit", self._freqtrade.strategy.order_types["exit"]) + sub_amount: Optional[float] = None + if amount and amount < trade.amount: + # Partial exit ... + min_exit_stake = self._freqtrade.exchange.get_min_pair_stake_amount( + trade.pair, current_rate, trade.stop_loss_pct) + remaining = (trade.amount - amount) * current_rate + if remaining < min_exit_stake: + raise RPCException(f'Remaining amount of {remaining} would be too small.') + sub_amount = amount + + self._freqtrade.execute_trade_exit( + trade, current_rate, exit_check, ordertype=order_type, + sub_trade_amt=sub_amount) + + def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *, + amount: Optional[float] = None) -> Dict[str, str]: """ Handler for forceexit . Sells the given trade at current price """ - def _exec_force_exit(trade: Trade) -> None: - # Check if there is there is an open order - fully_canceled = False - if trade.open_order_id: - order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) - - if order['side'] == trade.entry_side: - fully_canceled = self._freqtrade.handle_cancel_enter( - trade, order, CANCEL_REASON['FORCE_EXIT']) - - if order['side'] == trade.exit_side: - # Cancel order - so it is placed anew with a fresh price. - self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT']) - - if not fully_canceled: - # Get current rate and execute sell - current_rate = self._freqtrade.exchange.get_rate( - trade.pair, side='exit', is_short=trade.is_short, refresh=True) - exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT) - order_type = ordertype or self._freqtrade.strategy.order_types.get( - "force_exit", self._freqtrade.strategy.order_types["exit"]) - - self._freqtrade.execute_trade_exit( - trade, current_rate, exit_check, ordertype=order_type) - # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') @@ -698,7 +717,7 @@ class RPC: if trade_id == 'all': # Execute sell for all open orders for trade in Trade.get_open_trades(): - _exec_force_exit(trade) + self.__exec_force_exit(trade, ordertype) Trade.commit() self._freqtrade.wallets.update() return {'result': 'Created sell orders for all open trades.'} @@ -711,7 +730,7 @@ class RPC: logger.warning('force_exit: Invalid argument received') raise RPCException('invalid argument') - _exec_force_exit(trade) + self.__exec_force_exit(trade, ordertype, amount) Trade.commit() self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} @@ -720,7 +739,8 @@ class RPC: order_type: Optional[str] = None, order_side: SignalDirection = SignalDirection.LONG, stake_amount: Optional[float] = None, - enter_tag: Optional[str] = 'force_entry') -> Optional[Trade]: + enter_tag: Optional[str] = 'force_entry', + leverage: Optional[float] = None) -> Optional[Trade]: """ Handler for forcebuy Buys a pair trade at the given or current price @@ -762,6 +782,7 @@ class RPC: ordertype=order_type, trade=trade, is_short=is_short, enter_tag=enter_tag, + leverage_=leverage, ): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py new file mode 100644 index 000000000..7980b7ca2 --- /dev/null +++ b/freqtrade/util/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa: F401 +from freqtrade.util.ft_precise import FtPrecise +from freqtrade.util.periodic_cache import PeriodicCache diff --git a/freqtrade/util/ft_precise.py b/freqtrade/util/ft_precise.py new file mode 100644 index 000000000..aba0517a9 --- /dev/null +++ b/freqtrade/util/ft_precise.py @@ -0,0 +1,12 @@ +""" +Slim wrapper around ccxt's Precise (string math) +To have imports from freqtrade - and support float initializers +""" +from ccxt import Precise + + +class FtPrecise(Precise): + def __init__(self, number, decimals=None): + if not isinstance(number, str): + number = str(number) + super().__init__(number, decimals) diff --git a/freqtrade/configuration/PeriodicCache.py b/freqtrade/util/periodic_cache.py similarity index 100% rename from freqtrade/configuration/PeriodicCache.py rename to freqtrade/util/periodic_cache.py diff --git a/scripts/rest_client.py b/scripts/rest_client.py index e5d358c98..989e6a50d 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -275,14 +275,20 @@ class FtRestClient(): } return self._post("forceenter", data=data) - def forceexit(self, tradeid): + def forceexit(self, tradeid, ordertype=None, amount=None): """Force-exit a trade. :param tradeid: Id of the trade (can be received via status command) + :param ordertype: Order type to use (must be market or limit) + :param amount: Amount to sell. Full sell if not given :return: json object """ - return self._post("forceexit", data={"tradeid": tradeid}) + return self._post("forceexit", data={ + "tradeid": tradeid, + "ordertype": ordertype, + "amount": amount, + }) def strategies(self): """Lists available strategies diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 1a8cf3183..9642435e5 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -214,7 +214,8 @@ def mock_trade_4(fee, is_short: bool): open_order_id=f'prod_buy_{direc(is_short)}_12345', strategy='StrategyTestV3', timeframe=5, - is_short=is_short + is_short=is_short, + stop_loss_pct=0.10 ) o = Order.parse_from_ccxt_object(mock_order_4(is_short), 'ETC/BTC', entry_side(is_short)) trade.orders.append(o) @@ -270,7 +271,8 @@ def mock_trade_5(fee, is_short: bool): enter_tag='TEST1', stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455', timeframe=5, - is_short=is_short + is_short=is_short, + stop_loss_pct=0.10, ) o = Order.parse_from_ccxt_object(mock_order_5(is_short), 'XRP/BTC', entry_side(is_short)) trade.orders.append(o) diff --git a/tests/exchange/test_ccxt_precise.py b/tests/exchange/test_ccxt_precise.py index 026adb4c1..8b599093f 100644 --- a/tests/exchange/test_ccxt_precise.py +++ b/tests/exchange/test_ccxt_precise.py @@ -1,14 +1,14 @@ -from ccxt import Precise +from freqtrade.util import FtPrecise -ws = Precise('-1.123e-6') -ws = Precise('-1.123e-6') -xs = Precise('0.00000002') -ys = Precise('69696900000') -zs = Precise('0') +ws = FtPrecise('-1.123e-6') +ws = FtPrecise('-1.123e-6') +xs = FtPrecise('0.00000002') +ys = FtPrecise('69696900000') +zs = FtPrecise('0') -def test_precise(): +def test_FtPrecise(): assert ys * xs == '1393.938' assert xs * ys == '1393.938' @@ -45,31 +45,36 @@ def test_precise(): assert xs + zs == '0.00000002' assert ys + zs == '69696900000' - assert abs(Precise('-500.1')) == '500.1' - assert abs(Precise('213')) == '213' + assert abs(FtPrecise('-500.1')) == '500.1' + assert abs(FtPrecise('213')) == '213' - assert abs(Precise('-500.1')) == '500.1' - assert -Precise('213') == '-213' + assert abs(FtPrecise('-500.1')) == '500.1' + assert -FtPrecise('213') == '-213' - assert Precise('10.1') % Precise('0.5') == '0.1' - assert Precise('5550') % Precise('120') == '30' + assert FtPrecise('10.1') % FtPrecise('0.5') == '0.1' + assert FtPrecise('5550') % FtPrecise('120') == '30' - assert Precise('-0.0') == Precise('0') - assert Precise('5.534000') == Precise('5.5340') + assert FtPrecise('-0.0') == FtPrecise('0') + assert FtPrecise('5.534000') == FtPrecise('5.5340') - assert min(Precise('-3.1415'), Precise('-2')) == '-3.1415' + assert min(FtPrecise('-3.1415'), FtPrecise('-2')) == '-3.1415' - assert max(Precise('3.1415'), Precise('-2')) == '3.1415' + assert max(FtPrecise('3.1415'), FtPrecise('-2')) == '3.1415' - assert Precise('2') > Precise('1.2345') - assert not Precise('-3.1415') > Precise('-2') - assert not Precise('3.1415') > Precise('3.1415') - assert Precise.string_gt('3.14150000000000000000001', '3.1415') + assert FtPrecise('2') > FtPrecise('1.2345') + assert not FtPrecise('-3.1415') > FtPrecise('-2') + assert not FtPrecise('3.1415') > FtPrecise('3.1415') + assert FtPrecise.string_gt('3.14150000000000000000001', '3.1415') - assert Precise('3.1415') >= Precise('3.1415') - assert Precise('3.14150000000000000000001') >= Precise('3.1415') + assert FtPrecise('3.1415') >= FtPrecise('3.1415') + assert FtPrecise('3.14150000000000000000001') >= FtPrecise('3.1415') - assert not Precise('3.1415') < Precise('3.1415') + assert not FtPrecise('3.1415') < FtPrecise('3.1415') - assert Precise('3.1415') <= Precise('3.1415') - assert Precise('3.1415') <= Precise('3.14150000000000000000001') + assert FtPrecise('3.1415') <= FtPrecise('3.1415') + assert FtPrecise('3.1415') <= FtPrecise('3.14150000000000000000001') + + assert FtPrecise(213) == '213' + assert FtPrecise(-213) == '-213' + assert str(FtPrecise(-213)) == '-213' + assert FtPrecise(213.2) == '213.2' diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 5a83b964a..5213c1b36 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -203,7 +203,7 @@ def test_fetch_stoploss_order_ftx(default_conf, mocker, limit_sell_order, limit_ 'info': { 'orderId': 'mocked_limit_sell', }}]) - api_mock.fetch_order = MagicMock(return_value=limit_sell_order) + api_mock.fetch_order = MagicMock(return_value=limit_sell_order.copy()) # No orderId field - no call to fetch_order resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') @@ -219,11 +219,23 @@ def test_fetch_stoploss_order_ftx(default_conf, mocker, limit_sell_order, limit_ order = {'id': 'X', 'status': 'closed', 'info': {'orderId': None}, 'average': 0.254} api_mock.fetch_orders = MagicMock(return_value=[order]) api_mock.fetch_order.reset_mock() + api_mock.privateGetConditionalOrdersConditionalOrderIdTriggers = MagicMock( + return_value={'result': [ + {'orderId': 'mocked_market_sell', 'type': 'market', 'side': 'sell', 'price': 0.254} + ]}) resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') assert resp # fetch_order not called (no regular order ID) - assert api_mock.fetch_order.call_count == 0 - assert order == order + assert api_mock.fetch_order.call_count == 1 + api_mock.privateGetConditionalOrdersConditionalOrderIdTriggers.call_count == 1 + expected_resp = limit_sell_order.copy() + expected_resp.update({ + 'id_stop': 'X', + 'id': 'X', + 'type': 'stop', + 'status_stop': 'triggered', + }) + assert expected_resp == resp with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 4cebb6492..8a5356b3e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -305,6 +305,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): min_ago_open=800, min_ago_close=450, profit_rate=0.9, )) + Trade.commit() # Not locked with 1 trade assert not freqtrade.protections.global_stop() assert not freqtrade.protections.stop_per_pair('XRP/BTC') @@ -316,6 +317,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): min_ago_open=200, min_ago_close=120, profit_rate=0.9, )) + Trade.commit() # Not locked with 1 trade (first trade is outside of lookback_period) assert not freqtrade.protections.global_stop() assert not freqtrade.protections.stop_per_pair('XRP/BTC') @@ -327,14 +329,16 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, min_ago_open=20, min_ago_close=10, profit_rate=1.15, is_short=True )) + Trade.commit() assert freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side assert not PairLocks.is_pair_locked('XRP/BTC', side='*') assert PairLocks.is_pair_locked('XRP/BTC', side='long') == only_per_side Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=110, min_ago_close=20, profit_rate=0.8, + min_ago_open=110, min_ago_close=21, profit_rate=0.8, )) + Trade.commit() # Locks due to 2nd trade assert freqtrade.protections.global_stop() != only_per_side @@ -342,6 +346,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): assert PairLocks.is_pair_locked('XRP/BTC', side='long') assert PairLocks.is_pair_locked('XRP/BTC', side='*') != only_per_side assert not PairLocks.is_global_lock() + Trade.commit() @pytest.mark.usefixtures("init_persistence") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4c580c3c2..1a2428fe7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -461,46 +461,6 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: assert isnan(stats['profit_all_coin']) -# Test that rpc_trade_statistics can handle trades that lacks -# trade.open_rate (it is set to None) -def test_rpc_trade_statistics_closed(mocker, default_conf_usdt, ticker, fee): - mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', - return_value=1.1) - mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - get_fee=fee, - ) - - freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt) - patch_get_signal(freqtradebot) - stake_currency = default_conf_usdt['stake_currency'] - fiat_display_currency = default_conf_usdt['fiat_display_currency'] - - rpc = RPC(freqtradebot) - - # Create some test data - create_mock_trades_usdt(fee) - - for trade in Trade.query.order_by(Trade.id).all(): - trade.open_rate = None - - stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) - assert stats['profit_closed_coin'] == 0 - assert stats['profit_closed_percent_mean'] == 0 - assert stats['profit_closed_fiat'] == 0 - assert stats['profit_all_coin'] == 0 - assert stats['profit_all_percent_mean'] == 0 - assert stats['profit_all_fiat'] == 0 - assert stats['trade_count'] == 7 - assert stats['first_trade_date'] == '2 days ago' - assert stats['latest_trade_date'] == '17 minutes ago' - assert stats['avg_duration'] == '0:00:00' - assert stats['best_pair'] == 'XRP/USDT' - assert stats['best_rate'] == 10.0 - - def test_rpc_balance_handle_error(default_conf, mocker): mock_balance = { 'BTC': { diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b52bacf9c..a2b2cfedc 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1205,7 +1205,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets): fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets), - _is_dry_limit_order_filled=MagicMock(return_value=False), + _is_dry_limit_order_filled=MagicMock(return_value=True), ) patch_get_signal(ftbot) @@ -1215,12 +1215,27 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets): assert rc.json() == {"error": "Error querying /api/v1/forceexit: invalid argument"} Trade.query.session.rollback() - ftbot.enter_positions() + create_mock_trades(fee) + trade = Trade.get_trades([Trade.id == 5]).first() + assert pytest.approx(trade.amount) == 123 + rc = client_post(client, f"{BASE_URI}/forceexit", + data='{"tradeid": "5", "ordertype": "market", "amount": 23}') + assert_response(rc) + assert rc.json() == {'result': 'Created sell order for trade 5.'} + Trade.query.session.rollback() + + trade = Trade.get_trades([Trade.id == 5]).first() + assert pytest.approx(trade.amount) == 100 + assert trade.is_open is True rc = client_post(client, f"{BASE_URI}/forceexit", - data='{"tradeid": "1"}') + data='{"tradeid": "5"}') assert_response(rc) - assert rc.json() == {'result': 'Created sell order for trade 1.'} + assert rc.json() == {'result': 'Created sell order for trade 5.'} + Trade.query.session.rollback() + + trade = Trade.get_trades([Trade.id == 5]).first() + assert trade.is_open is False def test_api_pair_candles(botclient, ohlcv_history): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f274e2119..fb5fd38d8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -973,6 +973,14 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, trade.is_short = is_short assert pytest.approx(trade.stake_amount) == 500 + order['id'] = '55673' + + freqtrade.strategy.leverage.reset_mock() + assert freqtrade.execute_entry(pair, 200, leverage_=3) + assert freqtrade.strategy.leverage.call_count == 0 + trade = Trade.query.all()[10] + assert trade.leverage == 1 if trading_mode == 'spot' else 3 + @pytest.mark.parametrize("is_short", [False, True]) def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order, is_short) -> None: diff --git a/tests/test_integration.py b/tests/test_integration.py index 40fdb4277..6a11b13f4 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -291,7 +291,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, get_fee=fee, - amount_to_precision=lambda s, x, y: y, + amount_to_precision=lambda s, x, y: round(y, 4), price_to_precision=lambda s, x, y: y, ) @@ -303,6 +303,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert len(trade.orders) == 1 assert pytest.approx(trade.stake_amount) == 60 assert trade.open_rate == 2.02 + assert trade.orders[0].amount == trade.amount # No adjustment freqtrade.process() trade = Trade.get_trades().first() @@ -331,8 +332,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: trade = Trade.get_trades().first() assert len(trade.orders) == 2 assert pytest.approx(trade.stake_amount) == 120 - # assert trade.orders[0].amount == 30 - assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask'] + assert trade.orders[1].amount == round(60 / ticker_usdt_modif['ask'], 4) assert trade.amount == trade.orders[0].amount + trade.orders[1].amount assert trade.nr_of_successful_entries == 2 @@ -344,7 +344,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.is_open is False # assert trade.orders[0].amount == 30 assert trade.orders[0].side == 'sell' - assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask'] + assert trade.orders[1].amount == round(60 / ticker_usdt_modif['ask'], 4) # Sold everything assert trade.orders[-1].side == 'buy' assert trade.orders[2].amount == trade.amount diff --git a/tests/test_periodiccache.py b/tests/test_periodiccache.py index b2bd8ba2b..df05de4ef 100644 --- a/tests/test_periodiccache.py +++ b/tests/test_periodiccache.py @@ -1,6 +1,6 @@ import time_machine -from freqtrade.configuration import PeriodicCache +from freqtrade.util import PeriodicCache def test_ttl_cache():