Merge pull request #7216 from freqtrade/precise_calcs

Precise calcs
This commit is contained in:
Matthias 2022-08-17 14:32:02 +02:00 committed by GitHub
commit 83ca168bb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 279 additions and 203 deletions

View File

@ -9,12 +9,13 @@ from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.bybit import Bybit
from freqtrade.exchange.coinbasepro import Coinbasepro
from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
is_exchange_known_ccxt, is_exchange_officially_supported,
market_is_active, timeframe_to_minutes, timeframe_to_msecs,
timeframe_to_next_date, timeframe_to_prev_date,
timeframe_to_seconds, validate_exchange,
validate_exchanges)
from freqtrade.exchange.exchange import (amount_to_precision, available_exchanges, ccxt_exchanges,
date_minus_candles, is_exchange_known_ccxt,
is_exchange_officially_supported, market_is_active,
price_to_precision, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds,
validate_exchange, validate_exchanges)
from freqtrade.exchange.ftx import Ftx
from freqtrade.exchange.gateio import Gateio
from freqtrade.exchange.hitbtc import Hitbtc

View File

@ -681,45 +681,35 @@ class Exchange:
"""
return endpoint in self._api.has and self._api.has[endpoint]
def get_precision_amount(self, pair: str) -> Optional[float]:
"""
Returns the amount precision of the exchange.
:param pair: Pair to get precision for
:return: precision for amount or None. Must be used in combination with precisionMode
"""
return self.markets.get(pair, {}).get('precision', {}).get('amount', None)
def get_precision_price(self, pair: str) -> Optional[float]:
"""
Returns the price precision of the exchange.
:param pair: Pair to get precision for
:return: precision for price or None. Must be used in combination with precisionMode
"""
return self.markets.get(pair, {}).get('precision', {}).get('price', None)
def amount_to_precision(self, pair: str, amount: float) -> float:
"""
Returns the amount to buy or sell to a precision the Exchange accepts
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
"""
if self.markets[pair]['precision']['amount'] is not None:
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
precision=self.markets[pair]['precision']['amount'],
counting_mode=self.precisionMode,
))
return amount
"""
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
def price_to_precision(self, pair: str, price: float) -> float:
"""
Returns the price rounded up to the precision the Exchange accepts.
Partial Re-implementation of ccxt internal method decimal_to_precision(),
which does not support rounding up
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
align with amount_to_precision().
Rounds up
"""
if self.markets[pair]['precision']['price']:
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
# precision=self.markets[pair]['precision']['price'],
# counting_mode=self.precisionMode,
# ))
if self.precisionMode == TICK_SIZE:
precision = FtPrecise(self.markets[pair]['precision']['price'])
price_str = FtPrecise(price)
missing = price_str % precision
if not missing == FtPrecise("0"):
price = round(float(str(price_str - missing + precision)), 14)
else:
symbol_prec = self.markets[pair]['precision']['price']
big_price = price * pow(10, symbol_prec)
price = ceil(big_price) / pow(10, symbol_prec)
return price
return price_to_precision(price, self.get_precision_price(pair), self.precisionMode)
def price_get_one_pip(self, pair: str, price: float) -> float:
"""
@ -2862,3 +2852,61 @@ def market_is_active(market: Dict) -> bool:
# See https://github.com/ccxt/ccxt/issues/4874,
# https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520
return market.get('active', True) is not False
def amount_to_precision(amount: float, amount_precision: Optional[float],
precisionMode: Optional[int]) -> float:
"""
Returns the amount to buy or sell to a precision the Exchange accepts
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
:param amount: amount to truncate
:param amount_precision: amount precision to use.
should be retrieved from markets[pair]['precision']['amount']
:param precisionMode: precision mode to use. Should be used from precisionMode
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
:return: truncated amount
"""
if amount_precision is not None and precisionMode is not None:
precision = int(amount_precision) if precisionMode != TICK_SIZE else amount_precision
# precision must be an int for non-ticksize inputs.
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
precision=precision,
counting_mode=precisionMode,
))
return amount
def price_to_precision(price: float, price_precision: Optional[float],
precisionMode: Optional[int]) -> float:
"""
Returns the price rounded up to the precision the Exchange accepts.
Partial Re-implementation of ccxt internal method decimal_to_precision(),
which does not support rounding up
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
align with amount_to_precision().
!!! Rounds up
:param price: price to convert
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
:param precisionMode: precision mode to use. Should be used from precisionMode
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
:return: price rounded up to the precision the Exchange accepts
"""
if price_precision is not None and precisionMode is not None:
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
# precision=price_precision,
# counting_mode=self.precisionMode,
# ))
if precisionMode == TICK_SIZE:
precision = FtPrecise(price_precision)
price_str = FtPrecise(price)
missing = price_str % precision
if not missing == FtPrecise("0"):
price = round(float(str(price_str - missing + precision)), 14)
else:
symbol_prec = price_precision
big_price = price * pow(10, symbol_prec)
price = ceil(big_price) / pow(10, symbol_prec)
return price

View File

@ -7,9 +7,8 @@ from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.candletype import CandleType
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange import Exchange, date_minus_candles
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange import date_minus_candles
logger = logging.getLogger(__name__)

View File

@ -5,7 +5,6 @@ import copy
import logging
import traceback
from datetime import datetime, time, timedelta, timezone
from decimal import Decimal
from math import isclose
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple
@ -33,6 +32,7 @@ from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise
from freqtrade.wallets import Wallets
@ -159,6 +159,8 @@ class FreqtradeBot(LoggingMixin):
performs startup tasks
"""
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
# Update older trades with precision and precision mode
self.startup_backpopulate_precision()
if not self.edge:
# Adjust stoploss if it was changed
Trade.stoploss_reinitialization(self.strategy.stoploss)
@ -286,6 +288,17 @@ class FreqtradeBot(LoggingMixin):
else:
return 0.0
def startup_backpopulate_precision(self):
trades = Trade.get_trades([Trade.precision_mode.is_(None)])
for trade in trades:
if trade.exchange != self.exchange.id:
continue
trade.precision_mode = self.exchange.precisionMode
trade.amount_precision = self.exchange.get_precision_amount(trade.pair)
trade.price_precision = self.exchange.get_precision_price(trade.pair)
Trade.commit()
def startup_update_open_orders(self):
"""
Updates open orders based on order list kept in the database.
@ -565,7 +578,7 @@ class FreqtradeBot(LoggingMixin):
if stake_amount is not None and stake_amount < 0.0:
# We should decrease our position
amount = abs(float(Decimal(stake_amount) / Decimal(current_exit_rate)))
amount = abs(float(FtPrecise(stake_amount) / FtPrecise(current_exit_rate)))
if amount > trade.amount:
# This is currently ineffective as remaining would become < min tradable
# Fixing this would require checking for 0.0 there -
@ -738,7 +751,10 @@ class FreqtradeBot(LoggingMixin):
leverage=leverage,
is_short=is_short,
trading_mode=self.trading_mode,
funding_fees=funding_fees
funding_fees=funding_fees,
amount_precision=self.exchange.get_precision_amount(pair),
price_precision=self.exchange.get_precision_price(pair),
precision_mode=self.exchange.precisionMode,
)
else:
# This is additional buy, we reset fee_open_currency so timeout checking can work

View File

@ -1,20 +1,20 @@
from decimal import Decimal
from math import ceil
from freqtrade.exceptions import OperationalException
from freqtrade.util import FtPrecise
one = Decimal(1.0)
four = Decimal(4.0)
twenty_four = Decimal(24.0)
one = FtPrecise(1.0)
four = FtPrecise(4.0)
twenty_four = FtPrecise(24.0)
def interest(
exchange_name: str,
borrowed: Decimal,
rate: Decimal,
hours: Decimal
) -> Decimal:
borrowed: FtPrecise,
rate: FtPrecise,
hours: FtPrecise
) -> FtPrecise:
"""
Equation to calculate interest on margin trades
@ -31,13 +31,13 @@ def interest(
"""
exchange_name = exchange_name.lower()
if exchange_name == "binance":
return borrowed * rate * ceil(hours) / twenty_four
return borrowed * rate * FtPrecise(ceil(hours)) / twenty_four
elif exchange_name == "kraken":
# Rounded based on https://kraken-fees-calculator.github.io/
return borrowed * rate * (one + ceil(hours / four))
return borrowed * rate * (one + FtPrecise(ceil(hours / four)))
elif exchange_name == "ftx":
# As Explained under #Interest rates section in
# https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer
return borrowed * rate * ceil(hours) / twenty_four
return borrowed * rate * FtPrecise(ceil(hours)) / twenty_four
else:
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")

View File

@ -131,6 +131,7 @@ class Backtesting:
self.fee = config['fee']
else:
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
self.precision_mode = self.exchange.precisionMode
self.timerange = TimeRange.parse_timerange(
None if self.config.get('timerange') is None else str(self.config.get('timerange')))
@ -849,6 +850,9 @@ class Backtesting:
trading_mode=self.trading_mode,
leverage=leverage,
# interest_rate=interest_rate,
amount_precision=self.exchange.get_precision_amount(pair),
price_precision=self.exchange.get_precision_price(pair),
precision_mode=self.precision_mode,
orders=[],
)

View File

@ -130,6 +130,10 @@ def migrate_trades_and_orders_table(
get_column_def(cols, 'sell_order_status', 'null'))
amount_requested = get_column_def(cols, 'amount_requested', 'amount')
amount_precision = get_column_def(cols, 'amount_precision', 'null')
price_precision = get_column_def(cols, 'price_precision', 'null')
precision_mode = get_column_def(cols, 'precision_mode', 'null')
# Schema migration necessary
with engine.begin() as connection:
connection.execute(text(f"alter table trades rename to {trade_back_name}"))
@ -156,7 +160,8 @@ def migrate_trades_and_orders_table(
max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short,
interest_rate, funding_fees, realized_profit
interest_rate, funding_fees, realized_profit,
amount_precision, price_precision, precision_mode
)
select id, lower(exchange), pair, {base_currency} base_currency,
{stake_currency} stake_currency,
@ -182,7 +187,9 @@ def migrate_trades_and_orders_table(
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
{trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price,
{is_short} is_short, {interest_rate} interest_rate,
{funding_fees} funding_fees, {realized_profit} realized_profit
{funding_fees} funding_fees, {realized_profit} realized_profit,
{amount_precision} amount_precision, {price_precision} price_precision,
{precision_mode} precision_mode
from {trade_back_name}
"""))
@ -300,7 +307,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# Migrates both trades and orders table!
# if ('orders' not in previous_tables
# or not has_column(cols_orders, 'stop_price')):
if not has_column(cols_trades, 'realized_profit'):
if not has_column(cols_trades, 'precision_mode'):
logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}")
migrate_trades_and_orders_table(

View File

@ -3,7 +3,6 @@ This module contains the class to persist trades into SQLite
"""
import logging
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from math import isclose
from typing import Any, Dict, List, Optional
@ -15,8 +14,10 @@ from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPE
BuySell, LongShort)
from freqtrade.enums import ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import amount_to_precision, price_to_precision
from freqtrade.leverage import interest
from freqtrade.persistence.base import _DECL_BASE
from freqtrade.util import FtPrecise
logger = logging.getLogger(__name__)
@ -292,6 +293,9 @@ class LocalTrade():
timeframe: Optional[int] = None
trading_mode: TradingMode = TradingMode.SPOT
amount_precision: Optional[float] = None
price_precision: Optional[float] = None
precision_mode: Optional[int] = None
# Leverage trading properties
liquidation_price: Optional[float] = None
@ -523,9 +527,10 @@ class LocalTrade():
"""
Method used internally to set self.stop_loss.
"""
stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode)
if not self.stop_loss:
self.initial_stop_loss = stop_loss
self.stop_loss = stop_loss
self.initial_stop_loss = stop_loss_norm
self.stop_loss = stop_loss_norm
self.stop_loss_pct = -1 * abs(percent)
self.stoploss_last_update = datetime.utcnow()
@ -553,7 +558,8 @@ class LocalTrade():
# no stop loss assigned yet
if self.initial_stop_loss_pct is None or refresh:
self.__set_stop_loss(new_loss, stoploss)
self.initial_stop_loss = new_loss
self.initial_stop_loss = price_to_precision(
new_loss, self.price_precision, self.precision_mode)
self.initial_stop_loss_pct = -1 * abs(stoploss)
# evaluate if the stop loss needs to be updated
@ -617,7 +623,8 @@ class LocalTrade():
else:
logger.warning(
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
if isclose(order.safe_amount_after_fee, self.amount, abs_tol=MATH_CLOSE_PREC):
amount_tr = amount_to_precision(self.amount, self.amount_precision, self.precision_mode)
if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
self.close(order.safe_price)
else:
self.recalc_trade_from_orders()
@ -694,8 +701,8 @@ class LocalTrade():
Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees
"""
open_trade = Decimal(amount) * Decimal(open_rate)
fees = open_trade * Decimal(self.fee_open)
open_trade = FtPrecise(amount) * FtPrecise(open_rate)
fees = open_trade * FtPrecise(self.fee_open)
if self.is_short:
return float(open_trade - fees)
else:
@ -708,30 +715,30 @@ class LocalTrade():
"""
self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate)
def calculate_interest(self) -> Decimal:
def calculate_interest(self) -> FtPrecise:
"""
Calculate interest for this trade. Only applicable for Margin trading.
"""
zero = Decimal(0.0)
zero = FtPrecise(0.0)
# If nothing was borrowed
if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage:
return zero
open_date = self.open_date.replace(tzinfo=None)
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
sec_per_hour = Decimal(3600)
total_seconds = Decimal((now - open_date).total_seconds())
sec_per_hour = FtPrecise(3600)
total_seconds = FtPrecise((now - open_date).total_seconds())
hours = total_seconds / sec_per_hour or zero
rate = Decimal(self.interest_rate)
borrowed = Decimal(self.borrowed)
rate = FtPrecise(self.interest_rate)
borrowed = FtPrecise(self.borrowed)
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: Decimal, rate: float, fee: float) -> Decimal:
def _calc_base_close(self, amount: FtPrecise, rate: float, fee: float) -> FtPrecise:
close_trade = amount * Decimal(rate)
fees = close_trade * Decimal(fee)
close_trade = amount * FtPrecise(rate)
fees = close_trade * FtPrecise(fee)
if self.is_short:
return close_trade + fees
@ -747,7 +754,7 @@ class LocalTrade():
if rate is None and not self.close_rate:
return 0.0
amount1 = Decimal(amount or self.amount)
amount1 = FtPrecise(amount or self.amount)
trading_mode = self.trading_mode or TradingMode.SPOT
if trading_mode == TradingMode.SPOT:
@ -826,12 +833,12 @@ class LocalTrade():
return float(f"{profit_ratio:.8f}")
def recalc_trade_from_orders(self, is_closing: bool = False):
current_amount = 0.0
current_stake = 0.0
def recalc_trade_from_orders(self, *, is_closing: bool = False):
ZERO = FtPrecise(0.0)
current_amount = FtPrecise(0.0)
current_stake = FtPrecise(0.0)
total_stake = 0.0 # Total stake after all buy orders (does not subtract!)
avg_price = 0.0
avg_price = FtPrecise(0.0)
close_profit = 0.0
close_profit_abs = 0.0
@ -839,28 +846,29 @@ class LocalTrade():
if o.ft_is_open or not o.filled:
continue
tmp_amount = o.safe_amount_after_fee
tmp_price = o.safe_price
tmp_amount = FtPrecise(o.safe_amount_after_fee)
tmp_price = FtPrecise(o.safe_price)
is_exit = o.ft_order_side != self.entry_side
side = -1 if is_exit else 1
if tmp_amount > 0.0 and tmp_price is not None:
side = FtPrecise(-1 if is_exit else 1)
if tmp_amount > ZERO and tmp_price is not None:
current_amount += tmp_amount * side
price = avg_price if is_exit else tmp_price
current_stake += price * tmp_amount * side
if current_amount > 0:
if current_amount > ZERO:
avg_price = current_stake / current_amount
if is_exit:
# Process partial exits
exit_rate = o.safe_price
exit_amount = o.safe_amount_after_fee
profit = self.calc_profit(rate=exit_rate, amount=exit_amount, open_rate=avg_price)
profit = self.calc_profit(rate=exit_rate, amount=exit_amount,
open_rate=float(avg_price))
close_profit_abs += profit
close_profit = self.calc_profit_ratio(
exit_rate, amount=exit_amount, open_rate=avg_price)
if current_amount <= 0:
if current_amount <= ZERO:
profit = close_profit_abs
else:
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
@ -870,13 +878,15 @@ class LocalTrade():
self.realized_profit = close_profit_abs
self.close_profit_abs = profit
if current_amount > 0:
current_amount_tr = amount_to_precision(float(current_amount),
self.amount_precision, self.precision_mode)
if current_amount_tr > 0.0:
# Trade is still open
# Leverage not updated, as we don't allow changing leverage through DCA at the moment.
self.open_rate = current_stake / current_amount
self.stake_amount = current_stake / (self.leverage or 1.0)
self.amount = current_amount
self.fee_open_cost = self.fee_open * current_stake
self.open_rate = float(current_stake / current_amount)
self.amount = current_amount_tr
self.stake_amount = float(current_stake) / (self.leverage or 1.0)
self.fee_open_cost = self.fee_open * float(current_stake)
self.recalc_open_trade_value()
if self.stop_loss_pct is not None and self.open_rate is not None:
self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
@ -1119,6 +1129,9 @@ class Trade(_DECL_BASE, LocalTrade):
timeframe = Column(Integer, nullable=True)
trading_mode = Column(Enum(TradingMode), nullable=True)
amount_precision = Column(Float, nullable=True)
price_precision = Column(Float, nullable=True)
precision_mode = Column(Integer, nullable=True)
# Leverage trading properties
leverage = Column(Float, nullable=True, default=1.0)

View File

@ -49,7 +49,7 @@ setup(
],
install_requires=[
# from requirements.txt
'ccxt>=1.83.12',
'ccxt>=1.92.9',
'SQLAlchemy',
'python-telegram-bot>=13.4',
'arrow>=0.17.0',

View File

@ -81,7 +81,7 @@ def mock_trade_usdt_1(fee, is_short: bool):
def mock_order_usdt_2(is_short: bool):
return {
'id': f'1235_{direc(is_short)}',
'symbol': 'ETC/USDT',
'symbol': 'NEO/USDT',
'status': 'closed',
'side': entry_side(is_short),
'type': 'limit',
@ -95,7 +95,7 @@ def mock_order_usdt_2(is_short: bool):
def mock_order_usdt_2_exit(is_short: bool):
return {
'id': f'12366_{direc(is_short)}',
'symbol': 'ETC/USDT',
'symbol': 'NEO/USDT',
'status': 'closed',
'side': exit_side(is_short),
'type': 'limit',
@ -111,7 +111,7 @@ def mock_trade_usdt_2(fee, is_short: bool):
Closed trade...
"""
trade = Trade(
pair='ETC/USDT',
pair='NEO/USDT',
stake_amount=200.0,
amount=100.0,
amount_requested=100.0,
@ -132,10 +132,10 @@ def mock_trade_usdt_2(fee, is_short: bool):
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_2(is_short), 'ETC/USDT', entry_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_2(is_short), 'NEO/USDT', entry_side(is_short))
trade.orders.append(o)
o = Order.parse_from_ccxt_object(
mock_order_usdt_2_exit(is_short), 'ETC/USDT', exit_side(is_short))
mock_order_usdt_2_exit(is_short), 'NEO/USDT', exit_side(is_short))
trade.orders.append(o)
return trade
@ -205,7 +205,7 @@ def mock_trade_usdt_3(fee, is_short: bool):
def mock_order_usdt_4(is_short: bool):
return {
'id': f'prod_buy_12345_{direc(is_short)}',
'symbol': 'ETC/USDT',
'symbol': 'NEO/USDT',
'status': 'open',
'side': entry_side(is_short),
'type': 'limit',
@ -221,7 +221,7 @@ def mock_trade_usdt_4(fee, is_short: bool):
Simulate prod entry
"""
trade = Trade(
pair='ETC/USDT',
pair='NEO/USDT',
stake_amount=20.0,
amount=10.0,
amount_requested=10.01,
@ -236,7 +236,7 @@ def mock_trade_usdt_4(fee, is_short: bool):
timeframe=5,
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_4(is_short), 'ETC/USDT', entry_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_4(is_short), 'NEO/USDT', entry_side(is_short))
trade.orders.append(o)
return trade

View File

@ -78,3 +78,5 @@ def test_FtPrecise():
assert FtPrecise(-213) == '-213'
assert str(FtPrecise(-213)) == '-213'
assert FtPrecise(213.2) == '213.2'
assert float(FtPrecise(213.2)) == 213.2
assert float(FtPrecise(-213.2)) == -213.2

View File

@ -14,12 +14,12 @@ from pandas import DataFrame
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException,
OperationalException, PricingError, TemporaryError)
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_precision,
date_minus_candles, market_is_active, price_to_precision,
timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
calculate_backoff, remove_credentials)
from freqtrade.exchange.exchange import (date_minus_candles, market_is_active, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re
@ -279,62 +279,35 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
ex.validate_order_time_in_force(tif2)
@pytest.mark.parametrize("amount,precision_mode,precision,contract_size,expected,trading_mode", [
(2.34559, 2, 4, 1, 2.3455, 'spot'),
(2.34559, 2, 5, 1, 2.34559, 'spot'),
(2.34559, 2, 3, 1, 2.345, 'spot'),
(2.9999, 2, 3, 1, 2.999, 'spot'),
(2.9909, 2, 3, 1, 2.990, 'spot'),
(2.9909, 2, 0, 1, 2, 'spot'),
(29991.5555, 2, 0, 1, 29991, 'spot'),
(29991.5555, 2, -1, 1, 29990, 'spot'),
(29991.5555, 2, -2, 1, 29900, 'spot'),
@pytest.mark.parametrize("amount,precision_mode,precision,expected", [
(2.34559, 2, 4, 2.3455),
(2.34559, 2, 5, 2.34559),
(2.34559, 2, 3, 2.345),
(2.9999, 2, 3, 2.999),
(2.9909, 2, 3, 2.990),
(2.9909, 2, 0, 2),
(29991.5555, 2, 0, 29991),
(29991.5555, 2, -1, 29990),
(29991.5555, 2, -2, 29900),
# Tests for Tick-size
(2.34559, 4, 0.0001, 1, 2.3455, 'spot'),
(2.34559, 4, 0.00001, 1, 2.34559, 'spot'),
(2.34559, 4, 0.001, 1, 2.345, 'spot'),
(2.9999, 4, 0.001, 1, 2.999, 'spot'),
(2.9909, 4, 0.001, 1, 2.990, 'spot'),
(2.9909, 4, 0.005, 0.01, 2.99, 'futures'),
(2.9999, 4, 0.005, 10, 2.995, 'futures'),
(2.34559, 4, 0.0001, 2.3455),
(2.34559, 4, 0.00001, 2.34559),
(2.34559, 4, 0.001, 2.345),
(2.9999, 4, 0.001, 2.999),
(2.9909, 4, 0.001, 2.990),
(2.9909, 4, 0.005, 2.99),
(2.9999, 4, 0.005, 2.995),
])
def test_amount_to_precision(
default_conf,
mocker,
amount,
precision_mode,
precision,
contract_size,
expected,
trading_mode
):
def test_amount_to_precision(amount, precision_mode, precision, expected,):
"""
Test rounds down
"""
markets = PropertyMock(return_value={
'ETH/BTC': {
'contractSize': contract_size,
'precision': {
'amount': precision
}
}
})
default_conf['trading_mode'] = trading_mode
default_conf['margin_mode'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, id="binance")
# digits counting mode
# DECIMAL_PLACES = 2
# SIGNIFICANT_DIGITS = 3
# TICK_SIZE = 4
mocker.patch('freqtrade.exchange.Exchange.precisionMode',
PropertyMock(return_value=precision_mode))
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
pair = 'ETH/BTC'
assert exchange.amount_to_precision(pair, amount) == expected
assert amount_to_precision(amount, precision, precision_mode) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
@ -359,21 +332,13 @@ def test_amount_to_precision(
(0.000000003483, 4, 1e-12, 0.000000003483),
])
def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected):
"""Test price to precision"""
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': precision}}})
exchange = get_patched_exchange(mocker, default_conf, id="binance")
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
def test_price_to_precision(price, precision_mode, precision, expected):
# digits counting mode
# DECIMAL_PLACES = 2
# SIGNIFICANT_DIGITS = 3
# TICK_SIZE = 4
mocker.patch('freqtrade.exchange.Exchange.precisionMode',
PropertyMock(return_value=precision_mode))
pair = 'ETH/BTC'
assert exchange.price_to_precision(pair, price) == expected
assert price_to_precision(price, precision, precision_mode) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected", [

View File

@ -1,14 +1,14 @@
from decimal import Decimal
from math import isclose
import pytest
from freqtrade.leverage import interest
from freqtrade.util import FtPrecise
ten_mins = Decimal(1 / 6)
five_hours = Decimal(5.0)
twentyfive_hours = Decimal(25.0)
ten_mins = FtPrecise(1 / 6)
five_hours = FtPrecise(5.0)
twentyfive_hours = FtPrecise(25.0)
@pytest.mark.parametrize('exchange,interest_rate,hours,expected', [
@ -28,11 +28,11 @@ twentyfive_hours = Decimal(25.0)
('ftx', 0.00025, twentyfive_hours, 0.015625),
])
def test_interest(exchange, interest_rate, hours, expected):
borrowed = Decimal(60.0)
borrowed = FtPrecise(60.0)
assert isclose(interest(
exchange_name=exchange,
borrowed=borrowed,
rate=Decimal(interest_rate),
rate=FtPrecise(interest_rate),
hours=hours
), expected)

View File

@ -735,7 +735,7 @@ def test_PerformanceFilter_lookback(mocker, default_conf_usdt, fee, caplog) -> N
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
create_mock_trades_usdt(fee)
pm.refresh_pairlist()
assert pm.whitelist == ['XRP/USDT']
assert pm.whitelist == ['XRP/USDT', 'NEO/USDT']
assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
# Move to "outside" of lookback window, so original sorting is restored.
@ -762,8 +762,8 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
create_mock_trades_usdt(fee)
pm.refresh_pairlist()
assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT', 'LTC/USDT',
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', ]
assert pm.whitelist == ['XRP/USDT', 'NEO/USDT', 'ETH/USDT', 'LTC/USDT',
'TKN/USDT', 'ADA/USDT', 'ETC/USDT', ]
# assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
# Move to "outside" of lookback window, so original sorting is restored.

View File

@ -96,20 +96,20 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'profit_pct': -0.41,
'profit_abs': -4.09e-06,
'profit_fiat': ANY,
'stop_loss_abs': 9.882e-06,
'stop_loss_abs': 9.89e-06,
'stop_loss_pct': -10.0,
'stop_loss_ratio': -0.1,
'stoploss_order_id': None,
'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY,
'initial_stop_loss_abs': 9.882e-06,
'initial_stop_loss_abs': 9.89e-06,
'initial_stop_loss_pct': -10.0,
'initial_stop_loss_ratio': -0.1,
'stoploss_current_dist': -1.1080000000000002e-06,
'stoploss_current_dist_ratio': -0.10081893,
'stoploss_current_dist_pct': -10.08,
'stoploss_entry_dist': -0.00010475,
'stoploss_entry_dist_ratio': -0.10448878,
'stoploss_current_dist': pytest.approx(-1.0999999e-06),
'stoploss_current_dist_ratio': -0.10009099,
'stoploss_current_dist_pct': -10.01,
'stoploss_entry_dist': -0.00010402,
'stoploss_entry_dist_ratio': -0.10376381,
'open_order': None,
'realized_profit': 0.0,
'exchange': 'binance',
@ -181,20 +181,20 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'profit_pct': ANY,
'profit_abs': ANY,
'profit_fiat': ANY,
'stop_loss_abs': 9.882e-06,
'stop_loss_abs': 9.89e-06,
'stop_loss_pct': -10.0,
'stop_loss_ratio': -0.1,
'stoploss_order_id': None,
'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY,
'initial_stop_loss_abs': 9.882e-06,
'initial_stop_loss_abs': 9.89e-06,
'initial_stop_loss_pct': -10.0,
'initial_stop_loss_ratio': -0.1,
'stoploss_current_dist': ANY,
'stoploss_current_dist_ratio': ANY,
'stoploss_current_dist_pct': ANY,
'stoploss_entry_dist': -0.00010475,
'stoploss_entry_dist_ratio': -0.10448878,
'stoploss_entry_dist': -0.00010402,
'stoploss_entry_dist_ratio': -0.10376381,
'open_order': None,
'exchange': 'binance',
'realized_profit': 0.0,
@ -761,7 +761,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
# and trade amount is updated
rpc._rpc_force_exit('3')
assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount
assert pytest.approx(trade.amount) == filled_amount
mocker.patch(
'freqtrade.exchange.Exchange.fetch_order',
@ -830,7 +830,7 @@ def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
res = rpc._rpc_performance()
assert len(res) == 3
assert res[0]['pair'] == 'ETC/USDT'
assert res[0]['pair'] == 'NEO/USDT'
assert res[0]['count'] == 1
assert res[0]['profit_pct'] == 5.0

View File

@ -892,7 +892,7 @@ def test_api_performance(botclient, fee):
assert_response(rc)
assert len(rc.json()) == 2
assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_pct': 7.61,
'profit_ratio': 0.07609203, 'profit_abs': 0.01872279},
'profit_ratio': 0.07609203, 'profit_abs': 0.0187228},
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_pct': -5.57,
'profit_ratio': -0.05570419, 'profit_abs': -0.1150375}]

View File

@ -4,7 +4,6 @@
import logging
import time
from copy import deepcopy
from math import isclose
from typing import List
from unittest.mock import ANY, MagicMock, PropertyMock, patch
@ -12,7 +11,7 @@ import arrow
import pytest
from pandas import DataFrame
from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT
from freqtrade.constants import CANCEL_REASON, UNLIMITED_STAKE_AMOUNT
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RPCMessageType, RunMode,
SignalDirection, State)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
@ -23,9 +22,9 @@ from freqtrade.persistence import Order, PairLocks, Trade
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.protections.iprotection import ProtectionReturn
from freqtrade.worker import Worker
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker,
log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal,
patch_wallet, patch_whitelist)
from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot,
get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange,
patch_get_signal, patch_wallet, patch_whitelist)
from tests.conftest_trades import (MOCK_TRADE_COUNT, entry_side, exit_side, mock_order_1,
mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell,
mock_order_4, mock_order_5_stoploss, mock_order_6_sell)
@ -569,7 +568,7 @@ def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, lim
assert trade.open_date is not None
assert trade.exchange == 'binance'
assert trade.open_rate == ticker_usdt.return_value[ticker_side]
assert isclose(trade.amount, 60 / ticker_usdt.return_value[ticker_side])
assert pytest.approx(trade.amount) == 60 / ticker_usdt.return_value[ticker_side]
assert log_has(
f'{"Short" if is_short else "Long"} signal found: about create a new trade for ETH/USDT '
@ -1801,7 +1800,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
# stoploss initially at 20% as edge dictated it.
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert isclose(trade.stop_loss, 1.76)
assert pytest.approx(trade.stop_loss) == 1.76
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock()
@ -1818,7 +1817,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# stoploss should remain the same
assert isclose(trade.stop_loss, 1.76)
assert pytest.approx(trade.stop_loss) == 1.76
# stoploss on exchange should not be canceled
cancel_order_mock.assert_not_called()
@ -2172,7 +2171,7 @@ def test_handle_trade(
assert trade.close_rate == (2.0 if is_short else 2.2)
assert pytest.approx(trade.close_profit) == close_profit
assert trade.calc_profit(trade.close_rate) == 5.685
assert pytest.approx(trade.calc_profit(trade.close_rate)) == 5.685
assert trade.close_date is not None
assert trade.exit_reason == 'sell_signal1'
@ -4144,6 +4143,7 @@ def test_trailing_stop_loss_positive(
'last': enter_price + (-0.06 if is_short else 0.06),
})
)
caplog.clear()
# stop-loss not reached, adjusted stoploss
assert freqtrade.handle_trade(trade) is False
caplog_text = (f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: "
@ -4524,11 +4524,8 @@ def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_ord
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
# Amount changes by fee amount.
assert isclose(
freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj),
amount - (amount * 0.001),
abs_tol=MATH_CLOSE_PREC,
)
assert pytest.approx(freqtrade.get_real_amount(
trade, limit_buy_order_usdt, order_obj)) == amount - (amount * 0.001)
def test_get_real_amount_open_trade_usdt(default_conf_usdt, fee, mocker):
@ -4958,6 +4955,31 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s
assert hto_mock.call_args_list[1][0][0]['status'] == 'canceled'
@pytest.mark.usefixtures("init_persistence")
def test_startup_backpopulate_precision(mocker, default_conf_usdt, fee, caplog):
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
create_mock_trades_usdt(fee)
trades = Trade.get_trades().all()
trades[-1].exchange = 'some_other_exchange'
for trade in trades:
assert trade.price_precision is None
assert trade.amount_precision is None
assert trade.precision_mode is None
freqtrade.startup_backpopulate_precision()
trades = Trade.get_trades().all()
for trade in trades:
if trade.exchange == 'some_other_exchange':
assert trade.price_precision is None
assert trade.amount_precision is None
assert trade.precision_mode is None
else:
assert trade.price_precision is not None
assert trade.amount_precision is not None
assert trade.precision_mode is not None
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [False, True])
def test_update_closed_trades_without_assigned_fees(mocker, default_conf_usdt, fee, is_short):

View File

@ -189,7 +189,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
assert len(trades) == 5
for trade in trades:
assert trade.stake_amount == result1
assert pytest.approx(trade.stake_amount) == result1
# Reset trade open order id's
trade.open_order_id = None
trades = Trade.get_open_trades()
@ -220,8 +220,6 @@ def test_dca_buying(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,
price_to_precision=lambda s, x, y: y,
)
patch_get_signal(freqtrade)
@ -249,7 +247,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert len(trade.orders) == 2
for o in trade.orders:
assert o.status == "closed"
assert trade.stake_amount == 120
assert pytest.approx(trade.stake_amount) == 120
# Open-rate averaged between 2.0 and 2.0 * 0.995
assert trade.open_rate < 2.0
@ -259,11 +257,11 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 2
assert trade.stake_amount == 120
assert pytest.approx(trade.stake_amount) == 120
assert trade.orders[0].amount == 30
assert trade.orders[1].amount == 60 / ticker_usdt_modif['bid']
assert pytest.approx(trade.orders[1].amount) == 60 / ticker_usdt_modif['bid']
assert trade.amount == trade.orders[0].amount + trade.orders[1].amount
assert pytest.approx(trade.amount) == trade.orders[0].amount + trade.orders[1].amount
assert trade.nr_of_successful_buys == 2
assert trade.nr_of_successful_entries == 2
@ -274,7 +272,7 @@ def test_dca_buying(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 == 'buy'
assert trade.orders[1].amount == 60 / ticker_usdt_modif['bid']
assert pytest.approx(trade.orders[1].amount) == 60 / ticker_usdt_modif['bid']
# Sold everything
assert trade.orders[-1].side == 'sell'
assert trade.orders[2].amount == trade.amount

View File

@ -1387,7 +1387,8 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0",
caplog)
assert trade.open_trade_value == trade._calc_open_trade_value(trade.amount, trade.open_rate)
assert pytest.approx(trade.open_trade_value) == trade._calc_open_trade_value(
trade.amount, trade.open_rate)
assert trade.close_profit_abs is None
orders = trade.orders