Merge pull request #3239 from freqtrade/feat/fee_handling

Improve fee handling
This commit is contained in:
hroff-1902 2020-05-14 18:48:48 +03:00 committed by GitHub
commit 8e4ffea52b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 505 additions and 161 deletions

View File

@ -1,13 +1,20 @@
# SQL Helper
This page contains some help if you want to edit your sqlite db.
## Install sqlite3
**Ubuntu/Debian installation**
Sqlite3 is a terminal based sqlite application.
Feel free to use a visual Database editor like SqliteBrowser if you feel more comfortable with that.
### Ubuntu/Debian installation
```bash
sudo apt-get install sqlite3
```
## Open the DB
```bash
sqlite3
.open <filepath>
@ -16,45 +23,61 @@ sqlite3
## Table structure
### List tables
```bash
.tables
```
### Display table structure
```bash
.schema <table_name>
```
### Trade table structure
```sql
CREATE TABLE trades (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee_open FLOAT NOT NULL,
fee_close FLOAT NOT NULL,
open_rate FLOAT,
open_rate_requested FLOAT,
close_rate FLOAT,
close_rate_requested FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
stop_loss FLOAT,
initial_stop_loss FLOAT,
stoploss_order_id VARCHAR,
stoploss_last_update DATETIME,
max_rate FLOAT,
sell_reason VARCHAR,
strategy VARCHAR,
ticker_interval INTEGER,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
CREATE TABLE trades
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee_open FLOAT NOT NULL,
fee_open_cost FLOAT,
fee_open_currency VARCHAR,
fee_close FLOAT NOT NULL,
fee_close_cost FLOAT,
fee_close_currency VARCHAR,
open_rate FLOAT,
open_rate_requested FLOAT,
open_trade_price FLOAT,
close_rate FLOAT,
close_rate_requested FLOAT,
close_profit FLOAT,
close_profit_abs FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
stop_loss FLOAT,
stop_loss_pct FLOAT,
initial_stop_loss FLOAT,
initial_stop_loss_pct FLOAT,
stoploss_order_id VARCHAR,
stoploss_last_update DATETIME,
max_rate FLOAT,
min_rate FLOAT,
sell_reason VARCHAR,
strategy VARCHAR,
ticker_interval INTEGER,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);
CREATE INDEX ix_trades_stoploss_order_id ON trades (stoploss_order_id);
CREATE INDEX ix_trades_pair ON trades (pair);
CREATE INDEX ix_trades_is_open ON trades (is_open);
```
## Get all trades in the table

View File

@ -22,7 +22,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
from freqtrade.misc import deep_merge_dicts
from freqtrade.misc import deep_merge_dicts, safe_value_fallback
CcxtModuleType = Any
@ -471,26 +471,31 @@ class Exchange:
'pair': pair,
'price': rate,
'amount': _amount,
"cost": _amount * rate,
'cost': _amount * rate,
'type': ordertype,
'side': side,
'remaining': _amount,
'datetime': arrow.utcnow().isoformat(),
'status': "closed" if ordertype == "market" else "open",
'fee': None,
"info": {}
'info': {}
}
self._store_dry_order(dry_order)
self._store_dry_order(dry_order, pair)
# Copy order and close it - so the returned order is open unless it's a market order
return dry_order
def _store_dry_order(self, dry_order: Dict) -> None:
def _store_dry_order(self, dry_order: Dict, pair: str) -> None:
closed_order = dry_order.copy()
if closed_order["type"] in ["market", "limit"]:
if closed_order['type'] in ["market", "limit"]:
closed_order.update({
"status": "closed",
"filled": closed_order["amount"],
"remaining": 0
'status': 'closed',
'filled': closed_order['amount'],
'remaining': 0,
'fee': {
'currency': self.get_pair_quote_currency(pair),
'cost': dry_order['cost'] * self.get_fee(pair),
'rate': self.get_fee(pair)
}
})
if closed_order["type"] in ["stop_loss_limit"]:
closed_order["info"].update({"stopPrice": closed_order["price"]})
@ -1066,6 +1071,61 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
@staticmethod
def order_has_fee(order: Dict) -> bool:
"""
Verifies if the passed in order dict has the needed keys to extract fees,
and that these keys (currency, cost) are not empty.
:param order: Order or trade (one trade) dict
:return: True if the fee substructure contains currency and cost, false otherwise
"""
if not isinstance(order, dict):
return False
return ('fee' in order and order['fee'] is not None
and (order['fee'].keys() >= {'currency', 'cost'})
and order['fee']['currency'] is not None
and order['fee']['cost'] is not None
)
def calculate_fee_rate(self, order: Dict) -> Optional[float]:
"""
Calculate fee rate if it's not given by the exchange.
:param order: Order or trade (one trade) dict
"""
if order['fee'].get('rate') is not None:
return order['fee'].get('rate')
fee_curr = order['fee']['currency']
# Calculate fee based on order details
if fee_curr in self.get_pair_base_currency(order['symbol']):
# Base currency - divide by amount
return round(
order['fee']['cost'] / safe_value_fallback(order, order, 'filled', 'amount'), 8)
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
# Quote currency - divide by cost
return round(order['fee']['cost'] / order['cost'], 8)
else:
# If Fee currency is a different currency
try:
comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
tick = self.fetch_ticker(comb)
fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask')
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
except DependencyException:
return None
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
"""
Extract tuple of cost, currency, rate.
Requires order_has_fee to run first!
:param order: Order or trade (one trade) dict
:return: Tuple with cost, currency, rate of the given fee dict
"""
return (order['fee']['cost'],
order['fee']['currency'],
self.calculate_fee_rate(order))
# calculate rate ? (order['fee']['cost'] / (order['amount'] * order['price']))
def is_exchange_bad(exchange_name: str) -> bool:
return exchange_name in BAD_EXCHANGES

View File

@ -613,7 +613,7 @@ class FreqtradeBot:
trades_closed += 1
continue
# Check if we can sell our current pair
if trade.open_order_id is None and self.handle_trade(trade):
if trade.open_order_id is None and trade.is_open and self.handle_trade(trade):
trades_closed += 1
except DependencyException as exception:
@ -755,7 +755,7 @@ class FreqtradeBot:
# We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] == 'closed':
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
trade.update(stoploss_order)
self.update_trade_state(trade, stoploss_order, sl_order=True)
# Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair,
timeframe_to_next_date(self.config['ticker_interval']))
@ -1042,7 +1042,7 @@ class FreqtradeBot:
trade.sell_reason = sell_reason.value
# In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') == 'closed':
trade.update(order)
self.update_trade_state(trade, order)
Trade.session.flush()
# Lock pair for one candle to prevent immediate rebuys
@ -1133,7 +1133,7 @@ class FreqtradeBot:
#
def update_trade_state(self, trade: Trade, action_order: dict = None,
order_amount: float = None) -> bool:
order_amount: float = None, sl_order: bool = False) -> bool:
"""
Checks trades with open orders and updates the amount if necessary
Handles closing both buy and sell orders.
@ -1141,84 +1141,125 @@ class FreqtradeBot:
"""
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
logger.info('Found open order for %s', trade)
try:
order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair)
except InvalidOrderException as exception:
logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception)
return False
# Try update amount (binance-fix)
try:
new_amount = self.get_real_amount(trade, order, order_amount)
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount
order.pop('filled', None)
# Fee was applied, so set to 0
trade.fee_open = 0
trade.recalc_open_trade_price()
except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception)
order_id = trade.open_order_id
elif trade.stoploss_order_id and sl_order:
order_id = trade.stoploss_order_id
else:
return False
# Update trade with order values
logger.info('Found open order for %s', trade)
try:
order = action_order or self.exchange.get_order(order_id, trade.pair)
except InvalidOrderException as exception:
logger.warning('Unable to fetch order %s: %s', order_id, exception)
return False
# Try update amount (binance-fix)
try:
new_amount = self.get_real_amount(trade, order, order_amount)
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount
order.pop('filled', None)
trade.recalc_open_trade_price()
except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception)
if self.exchange.check_order_canceled_empty(order):
# Trade has been cancelled on exchange
# Handling of this will happen in check_handle_timeout.
return True
trade.update(order)
# Updating wallets when order is closed
if not trade.is_open:
self.wallets.update()
if self.exchange.check_order_canceled_empty(order):
# Trade has been cancelled on exchange
# Handling of this will happen in check_handle_timeout.
return True
trade.update(order)
# Updating wallets when order is closed
if not trade.is_open:
self.wallets.update()
return False
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
amount: float, fee_abs: float) -> float:
"""
Applies the fee to amount (either from Order or from Trades).
Can eat into dust if more than the required asset is available.
"""
self.wallets.update()
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
# Eat into dust if we own more than base currency
logger.info(f"Fee amount for {trade} was in base currency - "
f"Eating Fee {fee_abs} into dust.")
elif fee_abs != 0:
real_amount = self.exchange.amount_to_precision(trade.pair, amount - fee_abs)
logger.info(f"Applying fee on amount for {trade} "
f"(from {amount} to {real_amount}).")
return real_amount
return amount
def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float:
"""
Get real amount for the trade
Detect and update trade fee.
Calls trade.update_fee() uppon correct detection.
Returns modified amount if the fee was taken from the destination currency.
Necessary for exchanges which charge fees in base currency (e.g. binance)
:return: identical (or new) amount for the trade
"""
# Init variables
if order_amount is None:
order_amount = order['amount']
# Only run for closed orders
if trade.fee_open == 0 or order['status'] == 'open':
if trade.fee_updated(order.get('side', '')) or order['status'] == 'open':
return order_amount
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
# use fee from order-dict if possible
if ('fee' in order and order['fee'] is not None and
(order['fee'].keys() >= {'currency', 'cost'})):
if (order['fee']['currency'] is not None and
order['fee']['cost'] is not None and
trade_base_currency == order['fee']['currency']):
new_amount = order_amount - order['fee']['cost']
logger.info("Applying fee on amount for %s (from %s to %s) from Order",
trade, order['amount'], new_amount)
return new_amount
if self.exchange.order_has_fee(order):
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
# Fallback to Trades
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
if trade_base_currency == fee_currency:
# Apply fee to amount
return self.apply_fee_conditional(trade, trade_base_currency,
amount=order_amount, fee_abs=fee_cost)
return order_amount
return self.fee_detection_from_trades(trade, order, order_amount)
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float:
"""
fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
"""
trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair,
trade.open_date)
if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
return order_amount
fee_currency = None
amount = 0
fee_abs = 0
fee_abs = 0.0
fee_cost = 0.0
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
fee_rate_array: List[float] = []
for exectrade in trades:
amount += exectrade['amount']
if ("fee" in exectrade and exectrade['fee'] is not None and
(exectrade['fee'].keys() >= {'currency', 'cost'})):
if self.exchange.order_has_fee(exectrade):
fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade)
fee_cost += fee_cost_
if fee_rate_ is not None:
fee_rate_array.append(fee_rate_)
# only applies if fee is in quote currency!
if (exectrade['fee']['currency'] is not None and
exectrade['fee']['cost'] is not None and
trade_base_currency == exectrade['fee']['currency']):
fee_abs += exectrade['fee']['cost']
if trade_base_currency == fee_currency:
fee_abs += fee_cost_
# Ensure at least one trade was found:
if fee_currency:
# fee_rate should use mean
fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
raise DependencyException("Half bought? Amounts don't match")
real_amount = amount - fee_abs
if fee_abs != 0:
logger.info(f"Applying fee on amount for {trade} "
f"(from {order_amount} to {real_amount}) from Trades")
return real_amount
return self.apply_fee_conditional(trade, trade_base_currency,
amount=amount, fee_abs=fee_abs)
else:
return amount

View File

@ -86,11 +86,15 @@ def check_migrate(engine) -> None:
logger.debug(f'trying {table_back_name}')
# Check for latest column
if not has_column(cols, 'close_profit_abs'):
if not has_column(cols, 'fee_close_cost'):
logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee')
fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
fee_close = get_column_def(cols, 'fee_close', 'fee')
fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null')
fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null')
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
@ -120,7 +124,9 @@ def check_migrate(engine) -> None:
# Copy data back - following the correct schema
engine.execute(f"""insert into trades
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
(id, exchange, pair, is_open,
fee_open, fee_open_cost, fee_open_currency,
fee_close, fee_close_cost, fee_open_currency, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
@ -136,7 +142,9 @@ def check_migrate(engine) -> None:
else pair
end
pair,
is_open, {fee_open} fee_open, {fee_close} fee_close,
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
{fee_open_currency} fee_open_currency, {fee_close} fee_close,
{fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
open_rate, {open_rate_requested} open_rate_requested, close_rate,
{close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
@ -185,7 +193,11 @@ class Trade(_DECL_BASE):
pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True)
fee_open_currency = Column(String, nullable=True)
fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True)
fee_close_currency = Column(String, nullable=True)
open_rate = Column(Float)
open_rate_requested = Column(Float)
# open_trade_price - calculated via _calc_open_trade_price
@ -235,7 +247,11 @@ class Trade(_DECL_BASE):
'pair': self.pair,
'is_open': self.is_open,
'fee_open': self.fee_open,
'fee_open_cost': self.fee_open_cost,
'fee_open_currency': self.fee_open_currency,
'fee_close': self.fee_close,
'fee_close_cost': self.fee_close_cost,
'fee_close_currency': self.fee_close_currency,
'open_date_hum': arrow.get(self.open_date).humanize(),
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'close_date_hum': (arrow.get(self.close_date).humanize()
@ -360,6 +376,35 @@ class Trade(_DECL_BASE):
self
)
def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
side: str) -> None:
"""
Update Fee parameters. Only acts once per side
"""
if side == 'buy' and self.fee_open_currency is None:
self.fee_open_cost = fee_cost
self.fee_open_currency = fee_currency
if fee_rate is not None:
self.fee_open = fee_rate
# Assume close-fee will fall into the same fee category and take an educated guess
self.fee_close = fee_rate
elif side == 'sell' and self.fee_close_currency is None:
self.fee_close_cost = fee_cost
self.fee_close_currency = fee_currency
if fee_rate is not None:
self.fee_close = fee_rate
def fee_updated(self, side: str) -> bool:
"""
Verify if this side (buy / sell) has already been updated
"""
if side == 'buy':
return self.fee_open_currency is not None
elif side == 'sell':
return self.fee_close_currency is not None
else:
return False
def _calc_open_trade_price(self) -> float:
"""
Calculate the open_rate including open_fee.

View File

@ -780,7 +780,7 @@ def limit_buy_order():
'id': 'mocked_limit_buy',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
@ -796,7 +796,7 @@ def market_buy_order():
'id': 'mocked_market_buy',
'type': 'market',
'side': 'buy',
'pair': 'mocked',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'price': 0.00004099,
'amount': 91.99181073,
@ -812,7 +812,7 @@ def market_sell_order():
'id': 'mocked_limit_sell',
'type': 'market',
'side': 'sell',
'pair': 'mocked',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'price': 0.00004173,
'amount': 91.99181073,
@ -828,7 +828,7 @@ def limit_buy_order_old():
'id': 'mocked_limit_buy_old',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'symbol': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.00001099,
'amount': 90.99181073,
@ -844,7 +844,7 @@ def limit_sell_order_old():
'id': 'mocked_limit_sell_old',
'type': 'limit',
'side': 'sell',
'pair': 'ETH/BTC',
'symbol': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
@ -860,7 +860,7 @@ def limit_buy_order_old_partial():
'id': 'mocked_limit_buy_old_partial',
'type': 'limit',
'side': 'buy',
'pair': 'ETH/BTC',
'symbol': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
@ -874,7 +874,7 @@ def limit_buy_order_old_partial():
def limit_buy_order_old_partial_canceled(limit_buy_order_old_partial):
res = deepcopy(limit_buy_order_old_partial)
res['status'] = 'canceled'
res['fee'] = {'cost': 0.0001, 'currency': 'ETH'}
res['fee'] = {'cost': 0.023, 'currency': 'ETH'}
return res
@ -1585,7 +1585,7 @@ def buy_order_fee():
'id': 'mocked_limit_buy_old',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'symbol': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.245441,
'amount': 8.0,

View File

@ -2145,3 +2145,58 @@ def test_symbol_is_pair(market_symbol, base_currency, quote_currency, expected_r
])
def test_market_is_active(market, expected_result) -> None:
assert market_is_active(market) == expected_result
@pytest.mark.parametrize("order,expected", [
([{'fee'}], False),
({'fee': None}, False),
({'fee': {'currency': 'ETH/BTC'}}, False),
({'fee': {'currency': 'ETH/BTC', 'cost': None}}, False),
({'fee': {'currency': 'ETH/BTC', 'cost': 0.01}}, True),
])
def test_order_has_fee(order, expected) -> None:
assert Exchange.order_has_fee(order) == expected
@pytest.mark.parametrize("order,expected", [
({'symbol': 'ETH/BTC', 'fee': {'currency': 'ETH', 'cost': 0.43}},
(0.43, 'ETH', 0.01)),
({'symbol': 'ETH/USDT', 'fee': {'currency': 'USDT', 'cost': 0.01}},
(0.01, 'USDT', 0.01)),
({'symbol': 'BTC/USDT', 'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}},
(0.34, 'USDT', 0.01)),
])
def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
mocker.patch('freqtrade.exchange.Exchange.calculate_fee_rate', MagicMock(return_value=0.01))
ex = get_patched_exchange(mocker, default_conf)
assert ex.extract_cost_curr_rate(order) == expected
@pytest.mark.parametrize("order,expected", [
# Using base-currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, 0.1),
({'symbol': 'ETH/BTC', 'amount': 0.05, 'cost': 0.05,
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, 0.08),
# Using quote currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'BTC', 'cost': 0.005}}, 0.1),
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'BTC', 'cost': 0.002, 'rate': None}}, 0.04),
# Using foreign currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'NEO', 'cost': 0.0012}}, 0.001944),
({'symbol': 'ETH/BTC', 'amount': 2.21, 'cost': 0.02992561,
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, 0.00074305),
# TODO: More tests here!
# Rate included in return - return as is
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}}, 0.01),
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.005}}, 0.005),
])
def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
ex = get_patched_exchange(mocker, default_conf)
assert ex.calculate_fee_rate(order) == expected

View File

@ -51,7 +51,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_date_hum': ANY,
'is_open': ANY,
'fee_open': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': ANY,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY,
'open_trade_price': ANY,
'close_rate_requested': ANY,
@ -90,7 +94,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_date_hum': ANY,
'is_open': ANY,
'fee_open': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': ANY,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY,
'open_trade_price': ANY,
'close_rate_requested': ANY,

View File

@ -506,7 +506,11 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'close_rate_requested': None,
'current_rate': 1.099e-05,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'open_date': ANY,
'is_open': True,
'max_rate': 0.0,
@ -609,7 +613,11 @@ def test_api_forcebuy(botclient, mocker, fee):
'close_profit': None,
'close_rate_requested': None,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'is_open': False,
'max_rate': None,
'min_rate': None,

View File

@ -1140,7 +1140,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
'status': 'closed',
'type': 'stop_loss_limit',
'price': 3,
'average': 2
'average': 2,
'amount': limit_buy_order['amount'],
})
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit)
assert freqtrade.handle_stoploss_on_exchange(trade) is True
@ -2202,6 +2203,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
limit_buy_order_old_partial_canceled, mocker) -> None:
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0))
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@ -2221,7 +2223,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
# and apply fees if necessary.
freqtrade.check_handle_timedout()
assert log_has_re(r"Applying fee on amount for Trade.* Order", caplog)
assert log_has_re(r"Applying fee on amount for Trade.*", caplog)
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 2
@ -2229,9 +2231,10 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
assert len(trades) == 1
# Verify that trade has been updated
assert trades[0].amount == (limit_buy_order_old_partial['amount'] -
limit_buy_order_old_partial['remaining']) - 0.0001
limit_buy_order_old_partial['remaining']) - 0.023
assert trades[0].open_order_id is None
assert trades[0].fee_open == 0
assert trades[0].fee_updated('buy')
assert pytest.approx(trades[0].fee_open) == 0.001
def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, caplog, fee,
@ -3324,8 +3327,6 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
patch_RPCManager(mocker)
patch_exchange(mocker)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
pair='LTC/ETH',
@ -3336,21 +3337,43 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fe
fee_close=fee.return_value,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992) from Trades',
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
caplog)
def test_get_real_amount_quote_dust(default_conf, trades_for_order, buy_order_fee, fee,
caplog, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
walletmock = mocker.patch('freqtrade.wallets.Wallets.update')
mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=8.1122)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_order_id="123456"
)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
walletmock.reset_mock()
# Amount is kept as is
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
assert walletmock.call_count == 1
assert log_has_re(r'Fee amount for Trade.* was in base currency '
'- Eating Fee 0.008 into dust', caplog)
def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, fee):
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
patch_RPCManager(mocker)
patch_exchange(mocker)
amount = buy_order_fee['amount']
trade = Trade(
pair='LTC/ETH',
@ -3361,8 +3384,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f
fee_close=fee.return_value,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
@ -3374,8 +3396,6 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fee, mocker):
trades_for_order[0]['fee']['currency'] = 'ETH'
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
@ -3387,8 +3407,7 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fe
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
@ -3401,8 +3420,6 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_
limit_buy_order['fee'] = {'cost': 0.004, 'currency': None}
trades_for_order[0]['fee']['currency'] = None
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
@ -3414,8 +3431,7 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
@ -3425,8 +3441,6 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, fee,
trades_for_order[0]['fee']['currency'] = 'BNB'
trades_for_order[0]['fee']['cost'] = 0.00094518
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
@ -3438,16 +3452,13 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, fee,
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, fee, mocker):
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order2)
amount = float(sum(x['amount'] for x in trades_for_order2))
trade = Trade(
@ -3459,13 +3470,12 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992) from Trades',
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
caplog)
@ -3474,8 +3484,6 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
limit_buy_order = deepcopy(buy_order_fee)
limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'}
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order',
return_value=[trades_for_order])
amount = float(sum(x['amount'] for x in trades_for_order))
@ -3488,13 +3496,12 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996) from Order',
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).',
caplog)
@ -3502,8 +3509,6 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order
limit_buy_order = deepcopy(buy_order_fee)
limit_buy_order['fee'] = {'cost': 0.004}
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade(
@ -3515,8 +3520,7 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
@ -3526,8 +3530,6 @@ def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_
limit_buy_order = deepcopy(buy_order_fee)
limit_buy_order['amount'] = limit_buy_order['amount'] - 0.001
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade(
@ -3539,8 +3541,7 @@ def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_
fee_close=fee.return_value,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
with pytest.raises(DependencyException, match=r"Half bought\? Amounts don't match"):
@ -3553,8 +3554,6 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b
limit_buy_order = deepcopy(buy_order_fee)
trades_for_order[0]['amount'] = trades_for_order[0]['amount'] + 1e-15
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade(
@ -3566,8 +3565,7 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount changes by fee amount.
assert isclose(freqtrade.get_real_amount(trade, limit_buy_order), amount - (amount * 0.001),
@ -3578,8 +3576,6 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee,
# Remove "Currency" from fee dict
trades_for_order[0]['fee'] = {'cost': 0.008}
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
@ -3592,15 +3588,12 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
def test_get_real_amount_open_trade(default_conf, fee, mocker):
patch_RPCManager(mocker)
patch_exchange(mocker)
amount = 12345
trade = Trade(
pair='LTC/ETH',
@ -3615,12 +3608,41 @@ def test_get_real_amount_open_trade(default_conf, fee, mocker):
'id': 'mocked_order',
'amount': amount,
'status': 'open',
'side': 'buy',
}
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.get_real_amount(trade, order) == amount
@pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [
(8.0, 0.0, 10, 8),
(8.0, 0.0, 0, 8),
(8.0, 0.1, 0, 7.9),
(8.0, 0.1, 10, 8),
(8.0, 0.1, 8.0, 8.0),
(8.0, 0.1, 7.9, 7.9),
])
def test_apply_fee_conditional(default_conf, fee, caplog, mocker,
amount, fee_abs, wallet, amount_exp):
walletmock = mocker.patch('freqtrade.wallets.Wallets.update')
mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=wallet)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_order_id="123456"
)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
walletmock.reset_mock()
# Amount is kept as is
assert freqtrade.apply_fee_conditional(trade, 'LTC', amount, fee_abs) == amount_exp
assert walletmock.call_count == 1
def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker,
order_book_l2):
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True

View File

@ -44,6 +44,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
}
stoploss_order_closed = stoploss_order_open.copy()
stoploss_order_closed['status'] = 'closed'
stoploss_order_closed['filled'] = stoploss_order_closed['amount']
# Sell first trade based on stoploss, keep 2nd and 3rd trade open
stoploss_order_mock = MagicMock(
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
@ -67,7 +69,6 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
create_stoploss_order=MagicMock(return_value=True),
update_trade_state=MagicMock(),
_notify_sell=MagicMock(),
)
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
@ -97,8 +98,9 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
# Only order for 3rd trade needs to be cancelled
assert cancel_order_mock.call_count == 1
# Wallets must be updated between stoploss cancellation and selling.
assert wallets_mock.call_count == 2
# Wallets must be updated between stoploss cancellation and selling, and will be updated again
# during update_trade_state
assert wallets_mock.call_count == 4
trade = trades[0]
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
@ -144,7 +146,6 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
create_stoploss_order=MagicMock(return_value=True),
update_trade_state=MagicMock(),
_notify_sell=MagicMock(),
)
should_sell_mock = MagicMock(side_effect=[

View File

@ -465,6 +465,10 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.initial_stop_loss == 0.0
assert trade.open_trade_price == trade._calc_open_trade_price()
assert trade.close_profit_abs is None
assert trade.fee_open_cost is None
assert trade.fee_open_currency is None
assert trade.fee_close_cost is None
assert trade.fee_close_currency is None
trade = Trade.query.filter(Trade.id == 2).first()
assert trade.close_rate is not None
@ -741,7 +745,11 @@ def test_to_json(default_conf, fee):
'open_rate_requested': None,
'open_trade_price': 15.1668225,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'close_rate': None,
'close_rate_requested': None,
'amount': 123.0,
@ -790,7 +798,11 @@ def test_to_json(default_conf, fee):
'close_profit': None,
'close_rate_requested': None,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'is_open': None,
'max_rate': None,
'min_rate': None,
@ -862,6 +874,75 @@ def test_stoploss_reinitialization(default_conf, fee):
assert trade_adj.initial_stop_loss_pct == -0.04
def test_update_fee(fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
fee_open=fee.return_value,
open_date=arrow.utcnow().shift(hours=-2).datetime,
amount=10,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
max_rate=1,
)
fee_cost = 0.15
fee_currency = 'BTC'
fee_rate = 0.0075
assert trade.fee_open_currency is None
assert not trade.fee_updated('buy')
assert not trade.fee_updated('sell')
trade.update_fee(fee_cost, fee_currency, fee_rate, 'buy')
assert trade.fee_updated('buy')
assert not trade.fee_updated('sell')
assert trade.fee_open_currency == fee_currency
assert trade.fee_open_cost == fee_cost
assert trade.fee_open == fee_rate
# Setting buy rate should "guess" close rate
assert trade.fee_close == fee_rate
assert trade.fee_close_currency is None
assert trade.fee_close_cost is None
fee_rate = 0.0076
trade.update_fee(fee_cost, fee_currency, fee_rate, 'sell')
assert trade.fee_updated('buy')
assert trade.fee_updated('sell')
assert trade.fee_close == 0.0076
assert trade.fee_close_cost == fee_cost
assert trade.fee_close == fee_rate
def test_fee_updated(fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
fee_open=fee.return_value,
open_date=arrow.utcnow().shift(hours=-2).datetime,
amount=10,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
max_rate=1,
)
assert trade.fee_open_currency is None
assert not trade.fee_updated('buy')
assert not trade.fee_updated('sell')
assert not trade.fee_updated('asdf')
trade.update_fee(0.15, 'BTC', 0.0075, 'buy')
assert trade.fee_updated('buy')
assert not trade.fee_updated('sell')
assert trade.fee_open_currency is not None
assert trade.fee_close_currency is None
trade.update_fee(0.15, 'ABC', 0.0075, 'sell')
assert trade.fee_updated('buy')
assert trade.fee_updated('sell')
assert not trade.fee_updated('asfd')
@pytest.mark.usefixtures("init_persistence")
def test_total_open_trades_stakes(fee):