Updated tests to new persistence

This commit is contained in:
Sam Germain 2021-07-04 00:11:59 -06:00
parent 243173d2be
commit 6b20a315e3
8 changed files with 77 additions and 115 deletions

View File

@ -3,6 +3,7 @@ import logging
from typing import Dict from typing import Dict
import ccxt import ccxt
from decimal import Decimal
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
@ -89,3 +90,12 @@ class Binance(Exchange):
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from 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

View File

@ -8,6 +8,7 @@ import inspect
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from decimal import Decimal
from math import ceil from math import ceil
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@ -1473,6 +1474,18 @@ class Exchange:
self._async_get_trade_history(pair=pair, since=since, self._async_get_trade_history(pair=pair, since=since,
until=until, from_id=from_id)) 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: def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module) return exchange_name in ccxt_exchanges(ccxt_module)

View File

@ -3,6 +3,7 @@ import logging
from typing import Any, Dict from typing import Any, Dict
import ccxt import ccxt
from decimal import Decimal
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
@ -124,3 +125,11 @@ class Kraken(Exchange):
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from 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

View File

@ -49,9 +49,6 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
strategy = get_column_def(cols, 'strategy', 'null') strategy = get_column_def(cols, 'strategy', 'null')
leverage = get_column_def(cols, 'leverage', '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') interest_rate = get_column_def(cols, 'interest_rate', '0.0')
liquidation_price = get_column_def(cols, 'liquidation_price', 'null') liquidation_price = get_column_def(cols, 'liquidation_price', 'null')
is_short = get_column_def(cols, 'is_short', 'False') 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, stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy, max_rate, min_rate, sell_reason, sell_order_status, strategy,
timeframe, open_trade_value, close_profit_abs, timeframe, open_trade_value, close_profit_abs,
leverage, borrowed, borrowed_currency, collateral_currency, interest_rate, leverage, interest_rate,
liquidation_price, is_short liquidation_price, is_short
) )
select id, lower(exchange), 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, {sell_order_status} sell_order_status,
{strategy} strategy, {timeframe} timeframe, {strategy} strategy, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
{leverage} leverage, {borrowed} borrowed, {borrowed_currency} borrowed_currency, {leverage} leverage, {interest_rate} interest_rate,
{collateral_currency} collateral_currency, {interest_rate} interest_rate,
{liquidation_price} liquidation_price, {is_short} is_short {liquidation_price} liquidation_price, {is_short} is_short
from {table_back_name} from {table_back_name}
""")) """))

View File

@ -132,6 +132,9 @@ class Order(_DECL_BASE):
order_filled_date = Column(DateTime, nullable=True) order_filled_date = Column(DateTime, nullable=True)
order_update_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): def __repr__(self):
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' 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})') f'side={self.side}, order_type={self.order_type}, status={self.status})')
@ -261,61 +264,17 @@ class LocalTrade():
timeframe: Optional[int] = None timeframe: Optional[int] = None
# Margin trading properties # Margin trading properties
borrowed_currency: str = None
collateral_currency: str = None
interest_rate: float = 0.0 interest_rate: float = 0.0
liquidation_price: float = None liquidation_price: float = None
is_short: bool = False is_short: bool = False
borrowed: float = 0.0
leverage: float = None leverage: float = None
# @property @property
# def base_currency(self) -> str: def borrowed(self):
# if not self.pair: if not self.is_short:
# raise OperationalException('LocalTrade.pair must be assigned') return self.amount * (self.leverage-1)/self.leverage
# return self.pair.split("/")[1] else:
return self.amount
# 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 @property
def open_date_utc(self): def open_date_utc(self):
@ -326,13 +285,8 @@ class LocalTrade():
return self.close_date.replace(tzinfo=timezone.utc) return self.close_date.replace(tzinfo=timezone.utc)
def __init__(self, **kwargs): 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: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
if not self.is_short:
self.is_short = False
self.recalc_open_trade_value() self.recalc_open_trade_value()
def __repr__(self): def __repr__(self):
@ -404,9 +358,6 @@ class LocalTrade():
'max_rate': self.max_rate, 'max_rate': self.max_rate,
'leverage': self.leverage, 'leverage': self.leverage,
'borrowed': self.borrowed,
'borrowed_currency': self.borrowed_currency,
'collateral_currency': self.collateral_currency,
'interest_rate': self.interest_rate, 'interest_rate': self.interest_rate,
'liquidation_price': self.liquidation_price, 'liquidation_price': self.liquidation_price,
'is_short': self.is_short, 'is_short': self.is_short,
@ -473,7 +424,7 @@ class LocalTrade():
# evaluate if the stop loss needs to be updated # evaluate if the stop loss needs to be updated
else: 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): 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...") logger.debug(f"{self.pair} - Adjusting stoploss...")
self._set_new_stoploss(new_loss, stoploss) self._set_new_stoploss(new_loss, stoploss)
@ -510,15 +461,6 @@ class LocalTrade():
""" """
order_type = order['type'] 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 # Ignore open and cancelled orders
if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None: if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None:
return return
@ -531,11 +473,6 @@ class LocalTrade():
self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
self.amount = float(safe_value_fallback(order, 'filled', 'amount')) 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() self.recalc_open_trade_value()
if self.is_open: if self.is_open:
payment = "SELL" if self.is_short else "BUY" payment = "SELL" if self.is_short else "BUY"
@ -632,17 +569,16 @@ class LocalTrade():
: param interest_rate: interest_charge for borrowing this coin(optional). : param interest_rate: interest_charge for borrowing this coin(optional).
If interest_rate is not set self.interest_rate will be used 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) 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 return zero
open_date = self.open_date.replace(tzinfo=None) open_date = self.open_date.replace(tzinfo=None)
now = datetime.utcnow() now = datetime.utcnow()
# sec_per_day = Decimal(86400)
sec_per_hour = Decimal(3600) sec_per_hour = Decimal(3600)
total_seconds = Decimal((now - open_date).total_seconds()) total_seconds = Decimal((now - open_date).total_seconds())
# days = total_seconds/sec_per_day or zero
hours = total_seconds/sec_per_hour or zero hours = total_seconds/sec_per_hour or zero
rate = Decimal(interest_rate or self.interest_rate) 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): 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 return 0.0
else: 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: if self.is_short:
profit_ratio = ((self.open_trade_value - close_trade_value) / self.stake_amount) profit_ratio = ((self.open_trade_value - close_trade_value) / self.stake_amount)
else: else:
@ -907,14 +844,10 @@ class Trade(_DECL_BASE, LocalTrade):
timeframe = Column(Integer, nullable=True) timeframe = Column(Integer, nullable=True)
# Margin trading properties # 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) leverage = Column(Float, nullable=True)
borrowed = Column(Float, nullable=False, default=0.0)
interest_rate = Column(Float, nullable=False, default=0.0) interest_rate = Column(Float, nullable=False, default=0.0)
liquidation_price = Column(Float, nullable=True) liquidation_price = Column(Float, nullable=True)
is_short = Column(Boolean, nullable=False, default=False) 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 # End of margin trading properties
def __init__(self, **kwargs): def __init__(self, **kwargs):

View File

@ -2132,8 +2132,8 @@ def market_short_order():
'symbol': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'price': 0.00004173, 'price': 0.00004173,
'amount': 91.99181073, 'amount': 275.97543219,
'filled': 91.99181073, 'filled': 275.97543219,
'remaining': 0.0, 'remaining': 0.0,
'status': 'closed', 'status': 'closed',
'is_short': True, 'is_short': True,
@ -2151,8 +2151,8 @@ def market_exit_short_order():
'symbol': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'price': 0.00004099, 'price': 0.00004099,
'amount': 91.99181073, 'amount': 275.97543219,
'filled': 91.99181073, 'filled': 275.97543219,
'remaining': 0.0, 'remaining': 0.0,
'status': 'closed', 'status': 'closed',
# 'leverage': 3.0, # 'leverage': 3.0,

View File

@ -433,8 +433,7 @@ def leverage_trade(fee):
interest_rate: 0.05% per day interest_rate: 0.05% per day
open_rate: 0.123 base open_rate: 0.123 base
close_rate: 0.128 base close_rate: 0.128 base
amount: 123.0 crypto amount: 615 crypto
amount_with_leverage: 615.0
stake_amount: 15.129 base stake_amount: 15.129 base
borrowed: 60.516 base borrowed: 60.516 base
leverage: 5 leverage: 5

View File

@ -54,14 +54,13 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten
open_date=ten_minutes_ago, open_date=ten_minutes_ago,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
# borrowed=90.99181073, leverage=3.0,
interest_rate=0.0005, interest_rate=0.0005,
exchange='binance' exchange='binance'
) )
# assert trade.open_order_id is None # assert trade.open_order_id is None
assert trade.close_profit is None assert trade.close_profit is None
assert trade.close_date is None assert trade.close_date is None
assert trade.borrowed is None
assert trade.is_short is None assert trade.is_short is None
# trade.open_order_id = 'something' # trade.open_order_id = 'something'
trade.update(limit_short_order) trade.update(limit_short_order)
@ -101,7 +100,7 @@ def test_update_market_order(
interest_rate: 0.05% per 4 hrs interest_rate: 0.05% per 4 hrs
open_rate: 0.00004173 base open_rate: 0.00004173 base
close_rate: 0.00004099 base close_rate: 0.00004099 base
amount: 91.99181073 * leverage(3) = 275.97543219 crypto amount: = 275.97543219 crypto
stake_amount: 0.0038388182617629 stake_amount: 0.0038388182617629
borrowed: 275.97543219 crypto borrowed: 275.97543219 crypto
time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) 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_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
open_date=ten_minutes_ago, open_date=ten_minutes_ago,
leverage=3.0,
interest_rate=0.0005, interest_rate=0.0005,
exchange='kraken' exchange='kraken'
) )
@ -228,7 +228,7 @@ def test_trade_close(fee, five_hours_ago):
open_rate: 0.02 base open_rate: 0.02 base
close_rate: 0.01 base close_rate: 0.01 base
leverage: 3.0 leverage: 3.0
amount: 5 * 3 = 15 crypto amount: 15 crypto
borrowed: 15 crypto borrowed: 15 crypto
time-periods: 5 hours = 5/4 time-periods: 5 hours = 5/4
@ -286,13 +286,13 @@ def test_calc_close_trade_price_exception(limit_short_order, fee):
pair='ETH/BTC', pair='ETH/BTC',
stake_amount=0.001, stake_amount=0.001,
open_rate=0.1, open_rate=0.1,
amount=5, amount=15.0,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
exchange='binance', exchange='binance',
interest_rate=0.0005, interest_rate=0.0005,
is_short=True, leverage=3.0,
borrowed=15 is_short=True
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_short_order) trade.update(limit_short_order)
@ -306,6 +306,7 @@ def test_update_open_order(limit_short_order):
stake_amount=1.00, stake_amount=1.00,
open_rate=0.01, open_rate=0.01,
amount=5, amount=5,
leverage=3.0,
fee_open=0.1, fee_open=0.1,
fee_close=0.1, fee_close=0.1,
interest_rate=0.0005, 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 interest_rate: 0.05% per 4 hrs
open_rate: 0.00004173 base open_rate: 0.00004173 base
close_rate: 0.00001234 base close_rate: 0.00001234 base
amount: 91.99181073 * leverage(3) = 275.97543219 crypto amount: = 275.97543219 crypto
borrowed: 275.97543219 crypto borrowed: 275.97543219 crypto
time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) time-periods: 10 minutes(rounds up to 1 time-period of 4hrs)
interest: borrowed * interest_rate * time-periods 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 open_rate: 0.00004173 base
close_rate: 0.00004099 base close_rate: 0.00004099 base
stake_amount: 0.0038388182617629 stake_amount: 0.0038388182617629
amount: 91.99181073 * leverage(3) = 275.97543219 crypto amount: = 275.97543219 crypto
borrowed: 275.97543219 crypto borrowed: 275.97543219 crypto
time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) time-periods: 10 minutes(rounds up to 1 time-period of 4hrs)
5 hours = 5/4 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 open_rate: 0.00004173 base
close_rate: 0.00004099 base close_rate: 0.00004099 base
amount: amount:
91.99181073 * leverage(3) = 275.97543219 crypto 275.97543219 crypto
91.99181073 * leverage(5) = 459.95905365 crypto 459.95905365 crypto
borrowed: borrowed:
275.97543219 crypto 275.97543219 crypto
459.95905365 crypto 459.95905365 crypto
@ -512,7 +513,7 @@ def test_interest_kraken(market_short_order, ten_minutes_ago, five_hours_ago, fe
trade = Trade( trade = Trade(
pair='ETH/BTC', pair='ETH/BTC',
stake_amount=0.001, stake_amount=0.001,
amount=91.99181073, amount=275.97543219,
open_rate=0.00001099, open_rate=0.00001099,
open_date=ten_minutes_ago, open_date=ten_minutes_ago,
fee_open=fee.return_value, 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( trade = Trade(
pair='ETH/BTC', pair='ETH/BTC',
stake_amount=0.001, stake_amount=0.001,
amount=91.99181073, amount=459.95905365,
open_rate=0.00001099, open_rate=0.00001099,
open_date=five_hours_ago, open_date=five_hours_ago,
fee_open=fee.return_value, 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, fee_close=fee.return_value,
exchange='binance', exchange='binance',
is_short=True, is_short=True,
borrowed=275.97543219, leverage=3.0,
interest_rate=0.0005 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, fee_close=fee.return_value,
exchange='binance', exchange='binance',
is_short=True, is_short=True,
borrowed=459.95905365, leverage=5.0,
interest_rate=0.0005 interest_rate=0.0005
) )
@ -679,7 +680,8 @@ def test_stoploss_reinitialization(default_conf, fee):
exchange='binance', exchange='binance',
open_rate=1, open_rate=1,
max_rate=1, max_rate=1,
is_short=True is_short=True,
leverage=3.0,
) )
trade.adjust_stop_loss(trade.open_rate, -0.05, True) trade.adjust_stop_loss(trade.open_rate, -0.05, True)
assert trade.stop_loss == 1.05 assert trade.stop_loss == 1.05