Merge branch 'develop' into feat/freqai
This commit is contained in:
commit
df701b5862
@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = 'develop'
|
||||
__version__ = '2022.8.dev'
|
||||
|
||||
if 'dev' in __version__:
|
||||
try:
|
||||
|
@ -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
|
||||
|
@ -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__)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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__)
|
||||
|
@ -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):
|
||||
|
@ -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'])
|
||||
|
@ -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 <id>.
|
||||
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 <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()
|
||||
|
3
freqtrade/util/__init__.py
Normal file
3
freqtrade/util/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# flake8: noqa: F401
|
||||
from freqtrade.util.ft_precise import FtPrecise
|
||||
from freqtrade.util.periodic_cache import PeriodicCache
|
12
freqtrade/util/ft_precise.py
Normal file
12
freqtrade/util/ft_precise.py
Normal 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)
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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"))
|
||||
|
@ -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")
|
||||
|
@ -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': {
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
import time_machine
|
||||
|
||||
from freqtrade.configuration import PeriodicCache
|
||||
from freqtrade.util import PeriodicCache
|
||||
|
||||
|
||||
def test_ttl_cache():
|
||||
|
Loading…
Reference in New Issue
Block a user