From 6b20a315e38118357b4e52f0d4c534c436d3d974 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 4 Jul 2021 00:11:59 -0600 Subject: [PATCH] Updated tests to new persistence --- freqtrade/exchange/binance.py | 10 +++ freqtrade/exchange/exchange.py | 21 ++++-- freqtrade/exchange/kraken.py | 9 +++ freqtrade/persistence/migrations.py | 8 +-- freqtrade/persistence/models.py | 99 +++++------------------------ tests/conftest.py | 8 +-- tests/conftest_trades.py | 3 +- tests/test_persistence_short.py | 34 +++++----- 8 files changed, 77 insertions(+), 115 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 0c470cb24..a8d60d6c0 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -3,6 +3,7 @@ import logging from typing import Dict import ccxt +from decimal import Decimal from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) @@ -89,3 +90,12 @@ class Binance(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + @staticmethod + def calculate_interest(borrowed: Decimal, hours: Decimal, interest_rate: Decimal) -> Decimal: + # Rate is per day but accrued hourly or something + # binance: https://www.binance.com/en-AU/support/faq/360030157812 + one = Decimal(1) + twenty_four = Decimal(24) + # TODO-mg: Is hours rounded? + return borrowed * interest_rate * max(hours, one)/twenty_four diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 07ac337fc..1cc71c99f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -8,6 +8,7 @@ import inspect import logging from copy import deepcopy from datetime import datetime, timezone +from decimal import Decimal from math import ceil from typing import Any, Dict, List, Optional, Tuple @@ -551,7 +552,7 @@ class Exchange: amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', DEFAULT_AMOUNT_RESERVE_PERCENT) amount_reserve_percent = ( - amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 + amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 ) # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) @@ -965,7 +966,7 @@ class Exchange: logger.warning( "Buy Price from orderbook could not be determined." f"Orderbook: {order_book}" - ) + ) raise PricingError from e logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side " f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}") @@ -1252,8 +1253,8 @@ class Exchange: self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache ohlcv_df = ohlcv_to_dataframe( - ticks, timeframe, pair=pair, fill_missing=True, - drop_incomplete=self._ohlcv_partial_candle) + ticks, timeframe, pair=pair, fill_missing=True, + drop_incomplete=self._ohlcv_partial_candle) results_df[(pair, timeframe)] = ohlcv_df if cache: self._klines[(pair, timeframe)] = ohlcv_df @@ -1473,6 +1474,18 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) + @staticmethod + def calculate_interest(borrowed: Decimal, hours: Decimal, interest_rate: Decimal) -> Decimal: + """Generate the interest owed for borrowing an amount of currency for a certain amount of time + :param borrowed: The amount of currency borrowed + :param hours: The length of time in hours that the currency has been borrowed for + :param interest_rate: The rate of interest for this trade + #TODO: May update this just to the currency of the borrowed amount + :raises ValueError: Throws value error if not implemented for the exchange + :returns The amount of interest owed for the borrowed currency + """ + raise ValueError('Margin trading is not available on this exchange with freqtrade') + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 8f7cbe590..69be05416 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -3,6 +3,7 @@ import logging from typing import Any, Dict import ccxt +from decimal import Decimal from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) @@ -124,3 +125,11 @@ class Kraken(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + @staticmethod + def calculate_interest(borrowed: Decimal, hours: Decimal, interest_rate: Decimal) -> Decimal: + four = Decimal(4.0) + # https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading- + opening_fee = borrowed * interest_rate + roll_over_fee = borrowed * interest_rate * max(0, (hours-four)/four) + return opening_fee + roll_over_fee diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index efadc7467..48e053f8a 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -49,9 +49,6 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col strategy = get_column_def(cols, 'strategy', 'null') leverage = get_column_def(cols, 'leverage', 'null') - borrowed = get_column_def(cols, 'borrowed', '0.0') - borrowed_currency = get_column_def(cols, 'borrowed_currency', 'null') - collateral_currency = get_column_def(cols, 'collateral_currency', 'null') interest_rate = get_column_def(cols, 'interest_rate', '0.0') liquidation_price = get_column_def(cols, 'liquidation_price', 'null') is_short = get_column_def(cols, 'is_short', 'False') @@ -91,7 +88,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, sell_order_status, strategy, timeframe, open_trade_value, close_profit_abs, - leverage, borrowed, borrowed_currency, collateral_currency, interest_rate, + leverage, interest_rate, liquidation_price, is_short ) select id, lower(exchange), @@ -116,8 +113,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {sell_order_status} sell_order_status, {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, - {leverage} leverage, {borrowed} borrowed, {borrowed_currency} borrowed_currency, - {collateral_currency} collateral_currency, {interest_rate} interest_rate, + {leverage} leverage, {interest_rate} interest_rate, {liquidation_price} liquidation_price, {is_short} is_short from {table_back_name} """)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b3f2049da..d7d2aa0e3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,6 +132,9 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) + leverage = Column(Float, nullable=True, default=None) + is_short = Column(Boolean, nullable=True, default=False) + def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' f'side={self.side}, order_type={self.order_type}, status={self.status})') @@ -261,61 +264,17 @@ class LocalTrade(): timeframe: Optional[int] = None # Margin trading properties - borrowed_currency: str = None - collateral_currency: str = None interest_rate: float = 0.0 liquidation_price: float = None is_short: bool = False - borrowed: float = 0.0 leverage: float = None - # @property - # def base_currency(self) -> str: - # if not self.pair: - # raise OperationalException('LocalTrade.pair must be assigned') - # return self.pair.split("/")[1] - - # TODO: @samgermain: Amount should be persisted "as is". - # I've partially reverted this (this killed most of your tests) - # but leave this here as i'm not sure where you intended to use this. - # @property - # def amount(self) -> float: - # if self._leverage is not None: - # return self._amount * self.leverage - # else: - # return self._amount - - # @amount.setter - # def amount(self, value): - # self._amount = value - - # @property - # def borrowed(self) -> float: - # if self._leverage is not None: - # if self.is_short: - # # If shorting the full amount must be borrowed - # return self._amount * self._leverage - # else: - # # If not shorting, then the trader already owns a bit - # return self._amount * (self._leverage-1) - # else: - # return self._borrowed - - # @borrowed.setter - # def borrowed(self, value): - # self._borrowed = value - # self._leverage = None - - # @property - # def leverage(self) -> float: - # return self._leverage - - # @leverage.setter - # def leverage(self, value): - # self._leverage = value - # self._borrowed = None - - # End of margin trading properties + @property + def borrowed(self): + if not self.is_short: + return self.amount * (self.leverage-1)/self.leverage + else: + return self.amount @property def open_date_utc(self): @@ -326,13 +285,8 @@ class LocalTrade(): return self.close_date.replace(tzinfo=timezone.utc) def __init__(self, **kwargs): - if kwargs.get('leverage') and kwargs.get('borrowed'): - # TODO-mg: should I raise an error? - raise OperationalException('Cannot pass both borrowed and leverage to Trade') for key in kwargs: setattr(self, key, kwargs[key]) - if not self.is_short: - self.is_short = False self.recalc_open_trade_value() def __repr__(self): @@ -404,9 +358,6 @@ class LocalTrade(): 'max_rate': self.max_rate, 'leverage': self.leverage, - 'borrowed': self.borrowed, - 'borrowed_currency': self.borrowed_currency, - 'collateral_currency': self.collateral_currency, 'interest_rate': self.interest_rate, 'liquidation_price': self.liquidation_price, 'is_short': self.is_short, @@ -473,7 +424,7 @@ class LocalTrade(): # evaluate if the stop loss needs to be updated else: - # stop losses only walk up, never down!, #TODO: But adding more to a margin account would create a lower liquidation price, decreasing the minimum stoploss + # stop losses only walk up, never down!, #But adding more to a margin account would create a lower liquidation price, decreasing the minimum stoploss if (new_loss > self.stop_loss and not self.is_short) or (new_loss < self.stop_loss and self.is_short): logger.debug(f"{self.pair} - Adjusting stoploss...") self._set_new_stoploss(new_loss, stoploss) @@ -510,15 +461,6 @@ class LocalTrade(): """ order_type = order['type'] - if ('leverage' in order and 'borrowed' in order): - raise OperationalException( - 'Pass only one of Leverage or Borrowed to the order in update trade') - - if 'is_short' in order and order['side'] == 'sell': - # Only set's is_short on opening trades, ignores non-shorts - # TODO-mg: I don't like this, but it might be the only way - self.is_short = order['is_short'] - # Ignore open and cancelled orders if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None: return @@ -531,11 +473,6 @@ class LocalTrade(): self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.amount = float(safe_value_fallback(order, 'filled', 'amount')) - if 'borrowed' in order: - self.borrowed = order['borrowed'] - elif 'leverage' in order: - self.leverage = order['leverage'] - self.recalc_open_trade_value() if self.is_open: payment = "SELL" if self.is_short else "BUY" @@ -632,17 +569,16 @@ class LocalTrade(): : param interest_rate: interest_charge for borrowing this coin(optional). If interest_rate is not set self.interest_rate will be used """ - # TODO-mg: Need to set other conditions because sometimes self.open_date is not defined, but why would it ever not be set + zero = Decimal(0.0) - if not (self.borrowed): + # If nothing was borrowed + if (self.leverage == 1.0 and not self.is_short) or not self.leverage: return zero open_date = self.open_date.replace(tzinfo=None) now = datetime.utcnow() - # sec_per_day = Decimal(86400) sec_per_hour = Decimal(3600) total_seconds = Decimal((now - open_date).total_seconds()) - # days = total_seconds/sec_per_day or zero hours = total_seconds/sec_per_hour or zero rate = Decimal(interest_rate or self.interest_rate) @@ -746,7 +682,8 @@ class LocalTrade(): if (self.is_short and close_trade_value == 0.0) or (not self.is_short and self.open_trade_value == 0.0): return 0.0 else: - if self.borrowed: # TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else + # TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else + if (self.leverage == 1.0 and not self.is_short) or not self.leverage: if self.is_short: profit_ratio = ((self.open_trade_value - close_trade_value) / self.stake_amount) else: @@ -907,14 +844,10 @@ class Trade(_DECL_BASE, LocalTrade): timeframe = Column(Integer, nullable=True) # Margin trading properties - leverage = Column(Float, nullable=True) # TODO: can this be nullable, or should it default to 1? (must also be changed in migrations eventually) - borrowed = Column(Float, nullable=False, default=0.0) + leverage = Column(Float, nullable=True) interest_rate = Column(Float, nullable=False, default=0.0) liquidation_price = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) - # TODO: Bottom 2 might not be needed - borrowed_currency = Column(Float, nullable=True) - collateral_currency = Column(String(25), nullable=True) # End of margin trading properties def __init__(self, **kwargs): diff --git a/tests/conftest.py b/tests/conftest.py index 68d9430e0..3bd67e3e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2132,8 +2132,8 @@ def market_short_order(): 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'price': 0.00004173, - 'amount': 91.99181073, - 'filled': 91.99181073, + 'amount': 275.97543219, + 'filled': 275.97543219, 'remaining': 0.0, 'status': 'closed', 'is_short': True, @@ -2151,8 +2151,8 @@ def market_exit_short_order(): 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'price': 0.00004099, - 'amount': 91.99181073, - 'filled': 91.99181073, + 'amount': 275.97543219, + 'filled': 275.97543219, 'remaining': 0.0, 'status': 'closed', # 'leverage': 3.0, diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index bc728dd44..f9ebb9984 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -433,8 +433,7 @@ def leverage_trade(fee): interest_rate: 0.05% per day open_rate: 0.123 base close_rate: 0.128 base - amount: 123.0 crypto - amount_with_leverage: 615.0 + amount: 615 crypto stake_amount: 15.129 base borrowed: 60.516 base leverage: 5 diff --git a/tests/test_persistence_short.py b/tests/test_persistence_short.py index 759b25a1a..0debcf989 100644 --- a/tests/test_persistence_short.py +++ b/tests/test_persistence_short.py @@ -54,14 +54,13 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten open_date=ten_minutes_ago, fee_open=fee.return_value, fee_close=fee.return_value, - # borrowed=90.99181073, + leverage=3.0, interest_rate=0.0005, exchange='binance' ) # assert trade.open_order_id is None assert trade.close_profit is None assert trade.close_date is None - assert trade.borrowed is None assert trade.is_short is None # trade.open_order_id = 'something' trade.update(limit_short_order) @@ -101,7 +100,7 @@ def test_update_market_order( interest_rate: 0.05% per 4 hrs open_rate: 0.00004173 base close_rate: 0.00004099 base - amount: 91.99181073 * leverage(3) = 275.97543219 crypto + amount: = 275.97543219 crypto stake_amount: 0.0038388182617629 borrowed: 275.97543219 crypto time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) @@ -131,6 +130,7 @@ def test_update_market_order( fee_open=fee.return_value, fee_close=fee.return_value, open_date=ten_minutes_ago, + leverage=3.0, interest_rate=0.0005, exchange='kraken' ) @@ -228,7 +228,7 @@ def test_trade_close(fee, five_hours_ago): open_rate: 0.02 base close_rate: 0.01 base leverage: 3.0 - amount: 5 * 3 = 15 crypto + amount: 15 crypto borrowed: 15 crypto time-periods: 5 hours = 5/4 @@ -286,13 +286,13 @@ def test_calc_close_trade_price_exception(limit_short_order, fee): pair='ETH/BTC', stake_amount=0.001, open_rate=0.1, - amount=5, + amount=15.0, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', interest_rate=0.0005, - is_short=True, - borrowed=15 + leverage=3.0, + is_short=True ) trade.open_order_id = 'something' trade.update(limit_short_order) @@ -306,6 +306,7 @@ def test_update_open_order(limit_short_order): stake_amount=1.00, open_rate=0.01, amount=5, + leverage=3.0, fee_open=0.1, fee_close=0.1, interest_rate=0.0005, @@ -355,7 +356,7 @@ def test_calc_close_trade_price(market_short_order, market_exit_short_order, ten interest_rate: 0.05% per 4 hrs open_rate: 0.00004173 base close_rate: 0.00001234 base - amount: 91.99181073 * leverage(3) = 275.97543219 crypto + amount: = 275.97543219 crypto borrowed: 275.97543219 crypto time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) interest: borrowed * interest_rate * time-periods @@ -399,7 +400,7 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag open_rate: 0.00004173 base close_rate: 0.00004099 base stake_amount: 0.0038388182617629 - amount: 91.99181073 * leverage(3) = 275.97543219 crypto + amount: = 275.97543219 crypto borrowed: 275.97543219 crypto time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) 5 hours = 5/4 @@ -494,8 +495,8 @@ def test_interest_kraken(market_short_order, ten_minutes_ago, five_hours_ago, fe open_rate: 0.00004173 base close_rate: 0.00004099 base amount: - 91.99181073 * leverage(3) = 275.97543219 crypto - 91.99181073 * leverage(5) = 459.95905365 crypto + 275.97543219 crypto + 459.95905365 crypto borrowed: 275.97543219 crypto 459.95905365 crypto @@ -512,7 +513,7 @@ def test_interest_kraken(market_short_order, ten_minutes_ago, five_hours_ago, fe trade = Trade( pair='ETH/BTC', stake_amount=0.001, - amount=91.99181073, + amount=275.97543219, open_rate=0.00001099, open_date=ten_minutes_ago, fee_open=fee.return_value, @@ -531,7 +532,7 @@ def test_interest_kraken(market_short_order, ten_minutes_ago, five_hours_ago, fe trade = Trade( pair='ETH/BTC', stake_amount=0.001, - amount=91.99181073, + amount=459.95905365, open_rate=0.00001099, open_date=five_hours_ago, fee_open=fee.return_value, @@ -581,7 +582,7 @@ def test_interest_binance(market_short_order, ten_minutes_ago, five_hours_ago, f fee_close=fee.return_value, exchange='binance', is_short=True, - borrowed=275.97543219, + leverage=3.0, interest_rate=0.0005 ) @@ -599,7 +600,7 @@ def test_interest_binance(market_short_order, ten_minutes_ago, five_hours_ago, f fee_close=fee.return_value, exchange='binance', is_short=True, - borrowed=459.95905365, + leverage=5.0, interest_rate=0.0005 ) @@ -679,7 +680,8 @@ def test_stoploss_reinitialization(default_conf, fee): exchange='binance', open_rate=1, max_rate=1, - is_short=True + is_short=True, + leverage=3.0, ) trade.adjust_stop_loss(trade.open_rate, -0.05, True) assert trade.stop_loss == 1.05