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 # SQL Helper
This page contains some help if you want to edit your sqlite db. This page contains some help if you want to edit your sqlite db.
## Install sqlite3 ## 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 ```bash
sudo apt-get install sqlite3 sudo apt-get install sqlite3
``` ```
## Open the DB ## Open the DB
```bash ```bash
sqlite3 sqlite3
.open <filepath> .open <filepath>
@ -16,45 +23,61 @@ sqlite3
## Table structure ## Table structure
### List tables ### List tables
```bash ```bash
.tables .tables
``` ```
### Display table structure ### Display table structure
```bash ```bash
.schema <table_name> .schema <table_name>
``` ```
### Trade table structure ### Trade table structure
```sql ```sql
CREATE TABLE trades ( CREATE TABLE trades
id INTEGER NOT NULL, id INTEGER NOT NULL,
exchange VARCHAR NOT NULL, exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL, pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL, is_open BOOLEAN NOT NULL,
fee_open FLOAT NOT NULL, fee_open FLOAT NOT NULL,
fee_close FLOAT NOT NULL, fee_open_cost FLOAT,
open_rate FLOAT, fee_open_currency VARCHAR,
open_rate_requested FLOAT, fee_close FLOAT NOT NULL,
close_rate FLOAT, fee_close_cost FLOAT,
close_rate_requested FLOAT, fee_close_currency VARCHAR,
close_profit FLOAT, open_rate FLOAT,
stake_amount FLOAT NOT NULL, open_rate_requested FLOAT,
amount FLOAT, open_trade_price FLOAT,
open_date DATETIME NOT NULL, close_rate FLOAT,
close_date DATETIME, close_rate_requested FLOAT,
open_order_id VARCHAR, close_profit FLOAT,
stop_loss FLOAT, close_profit_abs FLOAT,
initial_stop_loss FLOAT, stake_amount FLOAT NOT NULL,
stoploss_order_id VARCHAR, amount FLOAT,
stoploss_last_update DATETIME, open_date DATETIME NOT NULL,
max_rate FLOAT, close_date DATETIME,
sell_reason VARCHAR, open_order_id VARCHAR,
strategy VARCHAR, stop_loss FLOAT,
ticker_interval INTEGER, stop_loss_pct FLOAT,
PRIMARY KEY (id), initial_stop_loss FLOAT,
CHECK (is_open IN (0, 1)) 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 ## 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, from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async 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 CcxtModuleType = Any
@ -471,26 +471,31 @@ class Exchange:
'pair': pair, 'pair': pair,
'price': rate, 'price': rate,
'amount': _amount, 'amount': _amount,
"cost": _amount * rate, 'cost': _amount * rate,
'type': ordertype, 'type': ordertype,
'side': side, 'side': side,
'remaining': _amount, 'remaining': _amount,
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'status': "closed" if ordertype == "market" else "open", 'status': "closed" if ordertype == "market" else "open",
'fee': None, '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 # Copy order and close it - so the returned order is open unless it's a market order
return dry_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() closed_order = dry_order.copy()
if closed_order["type"] in ["market", "limit"]: if closed_order['type'] in ["market", "limit"]:
closed_order.update({ closed_order.update({
"status": "closed", 'status': 'closed',
"filled": closed_order["amount"], 'filled': closed_order['amount'],
"remaining": 0 '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"]: if closed_order["type"] in ["stop_loss_limit"]:
closed_order["info"].update({"stopPrice": closed_order["price"]}) closed_order["info"].update({"stopPrice": closed_order["price"]})
@ -1066,6 +1071,61 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from 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: def is_exchange_bad(exchange_name: str) -> bool:
return exchange_name in BAD_EXCHANGES return exchange_name in BAD_EXCHANGES

View File

@ -613,7 +613,7 @@ class FreqtradeBot:
trades_closed += 1 trades_closed += 1
continue continue
# Check if we can sell our current pair # 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 trades_closed += 1
except DependencyException as exception: except DependencyException as exception:
@ -755,7 +755,7 @@ class FreqtradeBot:
# We check if stoploss order is fulfilled # We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] == 'closed': if stoploss_order and stoploss_order['status'] == 'closed':
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value 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 # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, self.strategy.lock_pair(trade.pair,
timeframe_to_next_date(self.config['ticker_interval'])) timeframe_to_next_date(self.config['ticker_interval']))
@ -1042,7 +1042,7 @@ class FreqtradeBot:
trade.sell_reason = sell_reason.value trade.sell_reason = sell_reason.value
# In case of market sell orders the order can be closed immediately # In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') == 'closed': if order.get('status', 'unknown') == 'closed':
trade.update(order) self.update_trade_state(trade, order)
Trade.session.flush() Trade.session.flush()
# Lock pair for one candle to prevent immediate rebuys # 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, 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 Checks trades with open orders and updates the amount if necessary
Handles closing both buy and sell orders. Handles closing both buy and sell orders.
@ -1141,84 +1141,125 @@ class FreqtradeBot:
""" """
# Get order details for actual price per unit # Get order details for actual price per unit
if trade.open_order_id: if trade.open_order_id:
# Update trade with order values order_id = trade.open_order_id
logger.info('Found open order for %s', trade) elif trade.stoploss_order_id and sl_order:
try: order_id = trade.stoploss_order_id
order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) else:
except InvalidOrderException as exception: return False
logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception) # Update trade with order values
return False logger.info('Found open order for %s', trade)
# Try update amount (binance-fix) try:
try: order = action_order or self.exchange.get_order(order_id, trade.pair)
new_amount = self.get_real_amount(trade, order, order_amount) except InvalidOrderException as exception:
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): logger.warning('Unable to fetch order %s: %s', order_id, exception)
order['amount'] = new_amount return False
order.pop('filled', None) # Try update amount (binance-fix)
# Fee was applied, so set to 0 try:
trade.fee_open = 0 new_amount = self.get_real_amount(trade, order, order_amount)
trade.recalc_open_trade_price() if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
except DependencyException as exception: order['amount'] = new_amount
logger.warning("Could not update trade amount: %s", exception) 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): if self.exchange.check_order_canceled_empty(order):
# Trade has been cancelled on exchange # Trade has been cancelled on exchange
# Handling of this will happen in check_handle_timeout. # Handling of this will happen in check_handle_timeout.
return True return True
trade.update(order) trade.update(order)
# Updating wallets when order is closed
if not trade.is_open:
self.wallets.update()
# Updating wallets when order is closed
if not trade.is_open:
self.wallets.update()
return False 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: 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) 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: if order_amount is None:
order_amount = order['amount'] order_amount = order['amount']
# Only run for closed orders # 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 return order_amount
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
# use fee from order-dict if possible # use fee from order-dict if possible
if ('fee' in order and order['fee'] is not None and if self.exchange.order_has_fee(order):
(order['fee'].keys() >= {'currency', 'cost'})): fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
if (order['fee']['currency'] is not None and logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
order['fee']['cost'] is not None and f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
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
# 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, trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair,
trade.open_date) trade.open_date)
if len(trades) == 0: if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
return order_amount return order_amount
fee_currency = None
amount = 0 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: for exectrade in trades:
amount += exectrade['amount'] amount += exectrade['amount']
if ("fee" in exectrade and exectrade['fee'] is not None and if self.exchange.order_has_fee(exectrade):
(exectrade['fee'].keys() >= {'currency', 'cost'})): 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! # only applies if fee is in quote currency!
if (exectrade['fee']['currency'] is not None and if trade_base_currency == fee_currency:
exectrade['fee']['cost'] is not None and fee_abs += fee_cost_
trade_base_currency == exectrade['fee']['currency']): # Ensure at least one trade was found:
fee_abs += exectrade['fee']['cost'] 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): if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
logger.warning(f"Amount {amount} does not match amount {trade.amount}") logger.warning(f"Amount {amount} does not match amount {trade.amount}")
raise DependencyException("Half bought? Amounts don't match") raise DependencyException("Half bought? Amounts don't match")
real_amount = amount - fee_abs
if fee_abs != 0: if fee_abs != 0:
logger.info(f"Applying fee on amount for {trade} " return self.apply_fee_conditional(trade, trade_base_currency,
f"(from {order_amount} to {real_amount}) from Trades") amount=amount, fee_abs=fee_abs)
return real_amount else:
return amount

View File

@ -86,11 +86,15 @@ def check_migrate(engine) -> None:
logger.debug(f'trying {table_back_name}') logger.debug(f'trying {table_back_name}')
# Check for latest column # 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}') logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee') 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 = 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') open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
stop_loss = get_column_def(cols, 'stop_loss', '0.0') 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 # Copy data back - following the correct schema
engine.execute(f"""insert into trades 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, open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id, stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
@ -136,7 +142,9 @@ def check_migrate(engine) -> None:
else pair else pair
end end
pair, 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, open_rate, {open_rate_requested} open_rate_requested, close_rate,
{close_rate_requested} close_rate_requested, close_profit, {close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id, 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) pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True, index=True) is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0) 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 = 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 = Column(Float)
open_rate_requested = Column(Float) open_rate_requested = Column(Float)
# open_trade_price - calculated via _calc_open_trade_price # open_trade_price - calculated via _calc_open_trade_price
@ -235,7 +247,11 @@ class Trade(_DECL_BASE):
'pair': self.pair, 'pair': self.pair,
'is_open': self.is_open, 'is_open': self.is_open,
'fee_open': self.fee_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': 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_hum': arrow.get(self.open_date).humanize(),
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'close_date_hum': (arrow.get(self.close_date).humanize() 'close_date_hum': (arrow.get(self.close_date).humanize()
@ -360,6 +376,35 @@ class Trade(_DECL_BASE):
self 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: def _calc_open_trade_price(self) -> float:
""" """
Calculate the open_rate including open_fee. Calculate the open_rate including open_fee.

View File

@ -780,7 +780,7 @@ def limit_buy_order():
'id': 'mocked_limit_buy', 'id': 'mocked_limit_buy',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'price': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
@ -796,7 +796,7 @@ def market_buy_order():
'id': 'mocked_market_buy', 'id': 'mocked_market_buy',
'type': 'market', 'type': 'market',
'side': 'buy', 'side': 'buy',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'price': 0.00004099, 'price': 0.00004099,
'amount': 91.99181073, 'amount': 91.99181073,
@ -812,7 +812,7 @@ def market_sell_order():
'id': 'mocked_limit_sell', 'id': 'mocked_limit_sell',
'type': 'market', 'type': 'market',
'side': 'sell', 'side': 'sell',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'price': 0.00004173, 'price': 0.00004173,
'amount': 91.99181073, 'amount': 91.99181073,
@ -828,7 +828,7 @@ def limit_buy_order_old():
'id': 'mocked_limit_buy_old', 'id': 'mocked_limit_buy_old',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime), 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
@ -844,7 +844,7 @@ def limit_sell_order_old():
'id': 'mocked_limit_sell_old', 'id': 'mocked_limit_sell_old',
'type': 'limit', 'type': 'limit',
'side': 'sell', 'side': 'sell',
'pair': 'ETH/BTC', 'symbol': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
@ -860,7 +860,7 @@ def limit_buy_order_old_partial():
'id': 'mocked_limit_buy_old_partial', 'id': 'mocked_limit_buy_old_partial',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'pair': 'ETH/BTC', 'symbol': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, '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): def limit_buy_order_old_partial_canceled(limit_buy_order_old_partial):
res = deepcopy(limit_buy_order_old_partial) res = deepcopy(limit_buy_order_old_partial)
res['status'] = 'canceled' res['status'] = 'canceled'
res['fee'] = {'cost': 0.0001, 'currency': 'ETH'} res['fee'] = {'cost': 0.023, 'currency': 'ETH'}
return res return res
@ -1585,7 +1585,7 @@ def buy_order_fee():
'id': 'mocked_limit_buy_old', 'id': 'mocked_limit_buy_old',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime), 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.245441, 'price': 0.245441,
'amount': 8.0, '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: def test_market_is_active(market, expected_result) -> None:
assert market_is_active(market) == expected_result 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, 'open_date_hum': ANY,
'is_open': ANY, 'is_open': ANY,
'fee_open': ANY, 'fee_open': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': ANY, 'fee_close': ANY,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY, 'open_rate_requested': ANY,
'open_trade_price': ANY, 'open_trade_price': ANY,
'close_rate_requested': ANY, 'close_rate_requested': ANY,
@ -90,7 +94,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_date_hum': ANY, 'open_date_hum': ANY,
'is_open': ANY, 'is_open': ANY,
'fee_open': ANY, 'fee_open': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': ANY, 'fee_close': ANY,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY, 'open_rate_requested': ANY,
'open_trade_price': ANY, 'open_trade_price': ANY,
'close_rate_requested': ANY, 'close_rate_requested': ANY,

View File

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

View File

@ -1140,7 +1140,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
'status': 'closed', 'status': 'closed',
'type': 'stop_loss_limit', 'type': 'stop_loss_limit',
'price': 3, 'price': 3,
'average': 2 'average': 2,
'amount': limit_buy_order['amount'],
}) })
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit)
assert freqtrade.handle_stoploss_on_exchange(trade) is True 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: limit_buy_order_old_partial_canceled, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) 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) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', '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. # and apply fees if necessary.
freqtrade.check_handle_timedout() 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 cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 2 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 assert len(trades) == 1
# Verify that trade has been updated # Verify that trade has been updated
assert trades[0].amount == (limit_buy_order_old_partial['amount'] - 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].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, 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): 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) 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) amount = sum(x['amount'] for x in trades_for_order)
trade = Trade( trade = Trade(
pair='LTC/ETH', 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, fee_close=fee.return_value,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount is reduced by "fee" # Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001) 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, ' 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) 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): 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=[]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
patch_RPCManager(mocker)
patch_exchange(mocker)
amount = buy_order_fee['amount'] amount = buy_order_fee['amount']
trade = Trade( trade = Trade(
pair='LTC/ETH', 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, fee_close=fee.return_value,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount is reduced by "fee" # Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount 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): def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fee, mocker):
trades_for_order[0]['fee']['currency'] = 'ETH' 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) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order) amount = sum(x['amount'] for x in trades_for_order)
trade = Trade( 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_rate=0.245441,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount does not change # Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount 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} limit_buy_order['fee'] = {'cost': 0.004, 'currency': None}
trades_for_order[0]['fee']['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) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order) amount = sum(x['amount'] for x in trades_for_order)
trade = Trade( 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_rate=0.245441,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount does not change # Amount does not change
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount 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']['currency'] = 'BNB'
trades_for_order[0]['fee']['cost'] = 0.00094518 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) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order) amount = sum(x['amount'] for x in trades_for_order)
trade = Trade( 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_rate=0.245441,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount does not change # Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount 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): 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) 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)) amount = float(sum(x['amount'] for x in trades_for_order2))
trade = Trade( 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_rate=0.245441,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount is reduced by "fee" # Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001) 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, ' 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) 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 = deepcopy(buy_order_fee)
limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'} limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'}
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order',
return_value=[trades_for_order]) return_value=[trades_for_order])
amount = float(sum(x['amount'] for x in 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_rate=0.245441,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount is reduced by "fee" # Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004 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, ' 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) 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 = deepcopy(buy_order_fee)
limit_buy_order['fee'] = {'cost': 0.004} limit_buy_order['fee'] = {'cost': 0.004}
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
amount = float(sum(x['amount'] for x in trades_for_order)) amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade( 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_rate=0.245441,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount does not change # Amount does not change
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount 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 = deepcopy(buy_order_fee)
limit_buy_order['amount'] = limit_buy_order['amount'] - 0.001 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) 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)) amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade( 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, fee_close=fee.return_value,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount does not change # Amount does not change
with pytest.raises(DependencyException, match=r"Half bought\? Amounts don't match"): 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) limit_buy_order = deepcopy(buy_order_fee)
trades_for_order[0]['amount'] = trades_for_order[0]['amount'] + 1e-15 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) 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)) amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade( 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_rate=0.245441,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount changes by fee amount. # Amount changes by fee amount.
assert isclose(freqtrade.get_real_amount(trade, limit_buy_order), amount - (amount * 0.001), 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 # Remove "Currency" from fee dict
trades_for_order[0]['fee'] = {'cost': 0.008} 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) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order) amount = sum(x['amount'] for x in trades_for_order)
trade = Trade( trade = Trade(
@ -3592,15 +3588,12 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee,
open_order_id="123456" open_order_id="123456"
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
# Amount does not change # Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
def test_get_real_amount_open_trade(default_conf, fee, mocker): def test_get_real_amount_open_trade(default_conf, fee, mocker):
patch_RPCManager(mocker)
patch_exchange(mocker)
amount = 12345 amount = 12345
trade = Trade( trade = Trade(
pair='LTC/ETH', pair='LTC/ETH',
@ -3615,12 +3608,41 @@ def test_get_real_amount_open_trade(default_conf, fee, mocker):
'id': 'mocked_order', 'id': 'mocked_order',
'amount': amount, 'amount': amount,
'status': 'open', 'status': 'open',
'side': 'buy',
} }
freqtrade = FreqtradeBot(default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade)
assert freqtrade.get_real_amount(trade, order) == amount 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, def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker,
order_book_l2): order_book_l2):
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True 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 = stoploss_order_open.copy()
stoploss_order_closed['status'] = 'closed' 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 # Sell first trade based on stoploss, keep 2nd and 3rd trade open
stoploss_order_mock = MagicMock( stoploss_order_mock = MagicMock(
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) 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( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
create_stoploss_order=MagicMock(return_value=True), create_stoploss_order=MagicMock(return_value=True),
update_trade_state=MagicMock(),
_notify_sell=MagicMock(), _notify_sell=MagicMock(),
) )
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) 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 # Only order for 3rd trade needs to be cancelled
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
# Wallets must be updated between stoploss cancellation and selling. # Wallets must be updated between stoploss cancellation and selling, and will be updated again
assert wallets_mock.call_count == 2 # during update_trade_state
assert wallets_mock.call_count == 4
trade = trades[0] trade = trades[0]
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value 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( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
create_stoploss_order=MagicMock(return_value=True), create_stoploss_order=MagicMock(return_value=True),
update_trade_state=MagicMock(),
_notify_sell=MagicMock(), _notify_sell=MagicMock(),
) )
should_sell_mock = MagicMock(side_effect=[ 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.initial_stop_loss == 0.0
assert trade.open_trade_price == trade._calc_open_trade_price() assert trade.open_trade_price == trade._calc_open_trade_price()
assert trade.close_profit_abs is None 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() trade = Trade.query.filter(Trade.id == 2).first()
assert trade.close_rate is not None assert trade.close_rate is not None
@ -741,7 +745,11 @@ def test_to_json(default_conf, fee):
'open_rate_requested': None, 'open_rate_requested': None,
'open_trade_price': 15.1668225, 'open_trade_price': 15.1668225,
'fee_close': 0.0025, 'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025, 'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'close_rate': None, 'close_rate': None,
'close_rate_requested': None, 'close_rate_requested': None,
'amount': 123.0, 'amount': 123.0,
@ -790,7 +798,11 @@ def test_to_json(default_conf, fee):
'close_profit': None, 'close_profit': None,
'close_rate_requested': None, 'close_rate_requested': None,
'fee_close': 0.0025, 'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025, 'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'is_open': None, 'is_open': None,
'max_rate': None, 'max_rate': None,
'min_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 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") @pytest.mark.usefixtures("init_persistence")
def test_total_open_trades_stakes(fee): def test_total_open_trades_stakes(fee):