Merge branch 'develop' into feat/freqai

This commit is contained in:
Matthias 2022-08-13 08:43:24 +02:00
commit df701b5862
23 changed files with 219 additions and 154 deletions

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = 'develop'
__version__ = '2022.8.dev'
if 'dev' in __version__:
try:

View File

@ -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

View File

@ -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__)

View File

@ -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

View File

@ -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)

View File

@ -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,8 +811,11 @@ 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)
if leverage_:
leverage = leverage_
else:
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=datetime.now(timezone.utc),
@ -818,7 +823,7 @@ class FreqtradeBot(LoggingMixin):
proposed_leverage=1.0,
max_leverage=max_leverage,
side=trade_side, entry_tag=entry_tag,
) if self.trading_mode != TradingMode.SPOT else 1.0
)
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
else:

View File

@ -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__)

View File

@ -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):

View File

@ -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'])

View File

@ -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,7 +241,10 @@ 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 = NAN
profit_str = f'{NAN:.2%}'
else:
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:
@ -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,6 +450,10 @@ class RPC:
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (PricingError, ExchangeError):
current_rate = NAN
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
@ -660,12 +667,8 @@ 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]:
"""
Handler for forceexit <id>.
Sells the given trade at current price
"""
def _exec_force_exit(trade: Trade) -> None:
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:
@ -686,10 +689,26 @@ class RPC:
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)
# ---- EOF def _exec_forcesell ----
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 <id>.
Sells the given trade at current price
"""
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 <asset> <price>
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()

View File

@ -0,0 +1,3 @@
# flake8: noqa: F401
from freqtrade.util.ft_precise import FtPrecise
from freqtrade.util.periodic_cache import PeriodicCache

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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'

View File

@ -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"))

View File

@ -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")

View File

@ -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': {

View File

@ -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):

View File

@ -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:

View File

@ -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

View File

@ -1,6 +1,6 @@
import time_machine
from freqtrade.configuration import PeriodicCache
from freqtrade.util import PeriodicCache
def test_ttl_cache():