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 """ """ Freqtrade bot """
__version__ = 'develop' __version__ = '2022.8.dev'
if 'dev' in __version__: if 'dev' in __version__:
try: 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_setup import setup_utils_configuration
from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.configuration.configuration import Configuration from freqtrade.configuration.configuration import Configuration
from freqtrade.configuration.PeriodicCache import PeriodicCache
from freqtrade.configuration.timerange import TimeRange 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 pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.configuration.PeriodicCache import PeriodicCache
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
from freqtrade.data.history import load_pair_history from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType, RunMode from freqtrade.enums import CandleType, RunMode
from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange, timeframe_to_seconds from freqtrade.exchange import Exchange, timeframe_to_seconds
from freqtrade.util import PeriodicCache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -16,7 +16,7 @@ import arrow
import ccxt import ccxt
import ccxt.async_support as ccxt_async import ccxt.async_support as ccxt_async
from cachetools import TTLCache 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 pandas import DataFrame
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell, 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) retrier_async)
from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2 from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.util import FtPrecise
CcxtModuleType = Any CcxtModuleType = Any
@ -708,10 +709,10 @@ class Exchange:
# counting_mode=self.precisionMode, # counting_mode=self.precisionMode,
# )) # ))
if self.precisionMode == TICK_SIZE: if self.precisionMode == TICK_SIZE:
precision = Precise(str(self.markets[pair]['precision']['price'])) precision = FtPrecise(self.markets[pair]['precision']['price'])
price_str = Precise(str(price)) price_str = FtPrecise(price)
missing = price_str % precision missing = price_str % precision
if not missing == Precise("0"): if not missing == FtPrecise("0"):
price = round(float(str(price_str - missing + precision)), 14) price = round(float(str(price_str - missing + precision)), 14)
else: else:
symbol_prec = self.markets[pair]['precision']['price'] symbol_prec = self.markets[pair]['precision']['price']
@ -849,6 +850,7 @@ class Exchange:
dry_order.update({ dry_order.update({
'average': average, 'average': average,
'filled': _amount, 'filled': _amount,
'remaining': 0.0,
'cost': (dry_order['amount'] * average) / leverage 'cost': (dry_order['amount'] * average) / leverage
}) })
# market orders will always incurr taker fees # market orders will always incurr taker fees

View File

@ -1,6 +1,6 @@
""" FTX exchange subclass """ """ FTX exchange subclass """
import logging import logging
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Optional, Tuple
import ccxt import ccxt
@ -116,9 +116,17 @@ class Ftx(Exchange):
if len(order) == 1: if len(order) == 1:
if order[0].get('status') == 'closed': if order[0].get('status') == 'closed':
# Trigger order was triggered ... # 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 # 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: if real_order_id:
order1 = self._api.fetch_order(real_order_id, pair) order1 = self._api.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order1) self._log_exchange_response('fetch_stoploss_order1', order1)

View File

@ -624,7 +624,8 @@ class FreqtradeBot(LoggingMixin):
ordertype: Optional[str] = None, ordertype: Optional[str] = None,
enter_tag: Optional[str] = None, enter_tag: Optional[str] = None,
trade: Optional[Trade] = None, trade: Optional[Trade] = None,
order_adjust: bool = False order_adjust: bool = False,
leverage_: Optional[float] = None,
) -> bool: ) -> bool:
""" """
Executes a limit buy for the given pair Executes a limit buy for the given pair
@ -640,7 +641,7 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None pos_adjust = trade is not None
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( 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: if not stake_amount:
return False return False
@ -787,6 +788,7 @@ class FreqtradeBot(LoggingMixin):
entry_tag: Optional[str], entry_tag: Optional[str],
trade: Optional[Trade], trade: Optional[Trade],
order_adjust: bool, order_adjust: bool,
leverage_: Optional[float],
) -> Tuple[float, float, float]: ) -> Tuple[float, float, float]:
if price: if price:
@ -809,16 +811,19 @@ class FreqtradeBot(LoggingMixin):
if not enter_limit_requested: if not enter_limit_requested:
raise PricingError('Could not determine entry price.') 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) max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( if leverage_:
pair=pair, leverage = leverage_
current_time=datetime.now(timezone.utc), else:
current_rate=enter_limit_requested, leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
proposed_leverage=1.0, pair=pair,
max_leverage=max_leverage, current_time=datetime.now(timezone.utc),
side=trade_side, entry_tag=entry_tag, current_rate=enter_limit_requested,
) if self.trading_mode != TradingMode.SPOT else 1.0 proposed_leverage=1.0,
max_leverage=max_leverage,
side=trade_side, entry_tag=entry_tag,
)
# Cap leverage between 1.0 and max_leverage. # Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage) leverage = min(max(leverage, 1.0), max_leverage)
else: else:

View File

@ -8,11 +8,11 @@ from typing import Any, Dict, List, Optional
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import PeriodicCache
from freqtrade.constants import ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.plugins.pairlist.IPairList import IPairList
from freqtrade.util import PeriodicCache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -194,11 +194,11 @@ class OrderSchema(BaseModel):
pair: str pair: str
order_id: str order_id: str
status: str status: str
remaining: float remaining: Optional[float]
amount: float amount: float
safe_price: float safe_price: float
cost: float cost: float
filled: float filled: Optional[float]
ft_order_side: str ft_order_side: str
order_type: str order_type: str
is_open: bool is_open: bool
@ -325,11 +325,13 @@ class ForceEnterPayload(BaseModel):
ordertype: Optional[OrderTypeValues] ordertype: Optional[OrderTypeValues]
stakeamount: Optional[float] stakeamount: Optional[float]
entry_tag: Optional[str] entry_tag: Optional[str]
leverage: Optional[float]
class ForceExitPayload(BaseModel): class ForceExitPayload(BaseModel):
tradeid: str tradeid: str
ordertype: Optional[OrderTypeValues] ordertype: Optional[OrderTypeValues]
amount: Optional[float]
class BlacklistPayload(BaseModel): class BlacklistPayload(BaseModel):

View File

@ -37,7 +37,8 @@ logger = logging.getLogger(__name__)
# 2.14: Add entry/exit orders to trade response # 2.14: Add entry/exit orders to trade response
# 2.15: Add backtest history endpoints # 2.15: Add backtest history endpoints
# 2.16: Additional daily metrics # 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. # Public API, requires no auth.
router_public = APIRouter() 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']) @router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading'])
def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None 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, trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
order_type=ordertype, stake_amount=stake_amount, order_type=ordertype, stake_amount=payload.stakeamount,
enter_tag=entry_tag) enter_tag=payload.entry_tag or 'force_entry',
leverage=payload.leverage)
if trade: if trade:
return ForceEnterResponse.parse_obj(trade.to_json()) 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']) @router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)): def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None 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']) @router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])

View File

@ -179,8 +179,10 @@ class RPC:
else: else:
current_rate = trade.close_rate current_rate = trade.close_rate
if len(trade.select_filled_orders(trade.entry_side)) > 0: if len(trade.select_filled_orders(trade.entry_side)) > 0:
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(
current_profit_abs = trade.calc_profit(current_rate) 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 current_profit_fiat: Optional[float] = None
# Calculate fiat profit # Calculate fiat profit
if self._fiat_converter: if self._fiat_converter:
@ -239,12 +241,15 @@ class RPC:
trade.pair, side='exit', is_short=trade.is_short, refresh=False) trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (PricingError, ExchangeError): except (PricingError, ExchangeError):
current_rate = NAN current_rate = NAN
if len(trade.select_filled_orders(trade.entry_side)) > 0: trade_profit = NAN
trade_profit = trade.calc_profit(current_rate) profit_str = f'{NAN:.2%}'
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
else: else:
trade_profit = 0.0 if trade.nr_of_successful_entries > 0:
profit_str = f'{0.0:.2f}' 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 '' direction_str = ('S' if trade.is_short else 'L') if nonspot else ''
if self._fiat_converter: if self._fiat_converter:
fiat_profit = self._fiat_converter.convert_amount( fiat_profit = self._fiat_converter.convert_amount(
@ -424,8 +429,6 @@ class RPC:
for trade in trades: for trade in trades:
current_rate: float = 0.0 current_rate: float = 0.0
if not trade.open_rate:
continue
if trade.close_date: if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds()) 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) trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (PricingError, ExchangeError): except (PricingError, ExchangeError):
current_rate = NAN current_rate = NAN
profit_ratio = trade.calc_profit_ratio(rate=current_rate) if isnan(current_rate):
profit_abs = trade.calc_profit( profit_ratio = NAN
rate=trade.close_rate or current_rate) + trade.realized_profit 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_coin.append(profit_abs)
profit_all_ratio.append(profit_ratio) 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.'} 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 <id>. Handler for forceexit <id>.
Sells the given trade at current price 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: if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running') raise RPCException('trader is not running')
@ -698,7 +717,7 @@ class RPC:
if trade_id == 'all': if trade_id == 'all':
# Execute sell for all open orders # Execute sell for all open orders
for trade in Trade.get_open_trades(): for trade in Trade.get_open_trades():
_exec_force_exit(trade) self.__exec_force_exit(trade, ordertype)
Trade.commit() Trade.commit()
self._freqtrade.wallets.update() self._freqtrade.wallets.update()
return {'result': 'Created sell orders for all open trades.'} return {'result': 'Created sell orders for all open trades.'}
@ -711,7 +730,7 @@ class RPC:
logger.warning('force_exit: Invalid argument received') logger.warning('force_exit: Invalid argument received')
raise RPCException('invalid argument') raise RPCException('invalid argument')
_exec_force_exit(trade) self.__exec_force_exit(trade, ordertype, amount)
Trade.commit() Trade.commit()
self._freqtrade.wallets.update() self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'} return {'result': f'Created sell order for trade {trade_id}.'}
@ -720,7 +739,8 @@ class RPC:
order_type: Optional[str] = None, order_type: Optional[str] = None,
order_side: SignalDirection = SignalDirection.LONG, order_side: SignalDirection = SignalDirection.LONG,
stake_amount: Optional[float] = None, 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> Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price Buys a pair trade at the given or current price
@ -762,6 +782,7 @@ class RPC:
ordertype=order_type, trade=trade, ordertype=order_type, trade=trade,
is_short=is_short, is_short=is_short,
enter_tag=enter_tag, enter_tag=enter_tag,
leverage_=leverage,
): ):
Trade.commit() Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() 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) return self._post("forceenter", data=data)
def forceexit(self, tradeid): def forceexit(self, tradeid, ordertype=None, amount=None):
"""Force-exit a trade. """Force-exit a trade.
:param tradeid: Id of the trade (can be received via status command) :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: json object
""" """
return self._post("forceexit", data={"tradeid": tradeid}) return self._post("forceexit", data={
"tradeid": tradeid,
"ordertype": ordertype,
"amount": amount,
})
def strategies(self): def strategies(self):
"""Lists available strategies """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', open_order_id=f'prod_buy_{direc(is_short)}_12345',
strategy='StrategyTestV3', strategy='StrategyTestV3',
timeframe=5, 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)) o = Order.parse_from_ccxt_object(mock_order_4(is_short), 'ETC/BTC', entry_side(is_short))
trade.orders.append(o) trade.orders.append(o)
@ -270,7 +271,8 @@ def mock_trade_5(fee, is_short: bool):
enter_tag='TEST1', enter_tag='TEST1',
stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455', stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455',
timeframe=5, 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)) o = Order.parse_from_ccxt_object(mock_order_5(is_short), 'XRP/BTC', entry_side(is_short))
trade.orders.append(o) 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 = FtPrecise('-1.123e-6')
ws = Precise('-1.123e-6') ws = FtPrecise('-1.123e-6')
xs = Precise('0.00000002') xs = FtPrecise('0.00000002')
ys = Precise('69696900000') ys = FtPrecise('69696900000')
zs = Precise('0') zs = FtPrecise('0')
def test_precise(): def test_FtPrecise():
assert ys * xs == '1393.938' assert ys * xs == '1393.938'
assert xs * ys == '1393.938' assert xs * ys == '1393.938'
@ -45,31 +45,36 @@ def test_precise():
assert xs + zs == '0.00000002' assert xs + zs == '0.00000002'
assert ys + zs == '69696900000' assert ys + zs == '69696900000'
assert abs(Precise('-500.1')) == '500.1' assert abs(FtPrecise('-500.1')) == '500.1'
assert abs(Precise('213')) == '213' assert abs(FtPrecise('213')) == '213'
assert abs(Precise('-500.1')) == '500.1' assert abs(FtPrecise('-500.1')) == '500.1'
assert -Precise('213') == '-213' assert -FtPrecise('213') == '-213'
assert Precise('10.1') % Precise('0.5') == '0.1' assert FtPrecise('10.1') % FtPrecise('0.5') == '0.1'
assert Precise('5550') % Precise('120') == '30' assert FtPrecise('5550') % FtPrecise('120') == '30'
assert Precise('-0.0') == Precise('0') assert FtPrecise('-0.0') == FtPrecise('0')
assert Precise('5.534000') == Precise('5.5340') 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 FtPrecise('2') > FtPrecise('1.2345')
assert not Precise('-3.1415') > Precise('-2') assert not FtPrecise('-3.1415') > FtPrecise('-2')
assert not Precise('3.1415') > Precise('3.1415') assert not FtPrecise('3.1415') > FtPrecise('3.1415')
assert Precise.string_gt('3.14150000000000000000001', '3.1415') assert FtPrecise.string_gt('3.14150000000000000000001', '3.1415')
assert Precise('3.1415') >= Precise('3.1415') assert FtPrecise('3.1415') >= FtPrecise('3.1415')
assert Precise('3.14150000000000000000001') >= Precise('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 FtPrecise('3.1415') <= FtPrecise('3.1415')
assert Precise('3.1415') <= Precise('3.14150000000000000000001') 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': { 'info': {
'orderId': 'mocked_limit_sell', '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 # No orderId field - no call to fetch_order
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') 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} order = {'id': 'X', 'status': 'closed', 'info': {'orderId': None}, 'average': 0.254}
api_mock.fetch_orders = MagicMock(return_value=[order]) api_mock.fetch_orders = MagicMock(return_value=[order])
api_mock.fetch_order.reset_mock() 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') resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
assert resp assert resp
# fetch_order not called (no regular order ID) # fetch_order not called (no regular order ID)
assert api_mock.fetch_order.call_count == 0 assert api_mock.fetch_order.call_count == 1
assert order == order 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): with pytest.raises(InvalidOrderException):
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) 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, min_ago_open=800, min_ago_close=450, profit_rate=0.9,
)) ))
Trade.commit()
# Not locked with 1 trade # Not locked with 1 trade
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not freqtrade.protections.stop_per_pair('XRP/BTC') 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, 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) # Not locked with 1 trade (first trade is outside of lookback_period)
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not freqtrade.protections.stop_per_pair('XRP/BTC') 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, '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 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 freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side
assert not PairLocks.is_pair_locked('XRP/BTC', side='*') assert not PairLocks.is_pair_locked('XRP/BTC', side='*')
assert PairLocks.is_pair_locked('XRP/BTC', side='long') == only_per_side assert PairLocks.is_pair_locked('XRP/BTC', side='long') == only_per_side
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, '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 # Locks due to 2nd trade
assert freqtrade.protections.global_stop() != only_per_side 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='long')
assert PairLocks.is_pair_locked('XRP/BTC', side='*') != only_per_side assert PairLocks.is_pair_locked('XRP/BTC', side='*') != only_per_side
assert not PairLocks.is_global_lock() assert not PairLocks.is_global_lock()
Trade.commit()
@pytest.mark.usefixtures("init_persistence") @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']) 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): def test_rpc_balance_handle_error(default_conf, mocker):
mock_balance = { mock_balance = {
'BTC': { 'BTC': {

View File

@ -1205,7 +1205,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
fetch_ticker=ticker, fetch_ticker=ticker,
get_fee=fee, get_fee=fee,
markets=PropertyMock(return_value=markets), 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) 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"} assert rc.json() == {"error": "Error querying /api/v1/forceexit: invalid argument"}
Trade.query.session.rollback() 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", rc = client_post(client, f"{BASE_URI}/forceexit",
data='{"tradeid": "1"}') data='{"tradeid": "5"}')
assert_response(rc) 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): 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 trade.is_short = is_short
assert pytest.approx(trade.stake_amount) == 500 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]) @pytest.mark.parametrize("is_short", [False, True])
def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order, is_short) -> None: 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', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt, fetch_ticker=ticker_usdt,
get_fee=fee, 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, 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 len(trade.orders) == 1
assert pytest.approx(trade.stake_amount) == 60 assert pytest.approx(trade.stake_amount) == 60
assert trade.open_rate == 2.02 assert trade.open_rate == 2.02
assert trade.orders[0].amount == trade.amount
# No adjustment # No adjustment
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() 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() trade = Trade.get_trades().first()
assert len(trade.orders) == 2 assert len(trade.orders) == 2
assert pytest.approx(trade.stake_amount) == 120 assert pytest.approx(trade.stake_amount) == 120
# assert trade.orders[0].amount == 30 assert trade.orders[1].amount == round(60 / ticker_usdt_modif['ask'], 4)
assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask']
assert trade.amount == trade.orders[0].amount + trade.orders[1].amount assert trade.amount == trade.orders[0].amount + trade.orders[1].amount
assert trade.nr_of_successful_entries == 2 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.is_open is False
# assert trade.orders[0].amount == 30 # assert trade.orders[0].amount == 30
assert trade.orders[0].side == 'sell' 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 # Sold everything
assert trade.orders[-1].side == 'buy' assert trade.orders[-1].side == 'buy'
assert trade.orders[2].amount == trade.amount assert trade.orders[2].amount == trade.amount

View File

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