Merge pull request #5157 from samgermain/margin-db

Margin db
This commit is contained in:
Matthias 2021-08-04 06:57:36 +02:00 committed by GitHub
commit 797d7e5ce6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1573 additions and 196 deletions

17
docs/leverage.md Normal file
View File

@ -0,0 +1,17 @@
# Leverage
For shorts, the currency which pays the interest fee for the `borrowed` currency is purchased at the same time of the closing trade (This means that the amount purchased in short closing trades is greater than the amount sold in short opening trades).
For longs, the currency which pays the interest fee for the `borrowed` will already be owned by the user and does not need to be purchased. The interest is subtracted from the close_value of the trade.
## Binance margin trading interest formula
I (interest) = P (borrowed money) * R (daily_interest/24) * ceiling(T) (in hours)
[source](https://www.binance.com/en/support/faq/360030157812)
## Kraken margin trading interest formula
Opening fee = P (borrowed money) * R (quat_hourly_interest)
Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours)
I (interest) = Opening fee + Rollover fee
[source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-)

View File

@ -1,5 +1,6 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.enums.backteststate import BacktestState from freqtrade.enums.backteststate import BacktestState
from freqtrade.enums.interestmode import InterestMode
from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.rpcmessagetype import RPCMessageType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.selltype import SellType from freqtrade.enums.selltype import SellType

View File

@ -0,0 +1,28 @@
from decimal import Decimal
from enum import Enum
from math import ceil
from freqtrade.exceptions import OperationalException
one = Decimal(1.0)
four = Decimal(4.0)
twenty_four = Decimal(24.0)
class InterestMode(Enum):
"""Equations to calculate interest"""
HOURSPERDAY = "HOURSPERDAY"
HOURSPER4 = "HOURSPER4" # Hours per 4 hour segment
NONE = "NONE"
def __call__(self, borrowed: Decimal, rate: Decimal, hours: Decimal):
if self.name == "HOURSPERDAY":
return borrowed * rate * ceil(hours)/twenty_four
elif self.name == "HOURSPER4":
# Rounded based on https://kraken-fees-calculator.github.io/
return borrowed * rate * (1+ceil(hours/four))
else:
raise OperationalException("Leverage not available on this exchange with freqtrade")

View File

@ -268,7 +268,7 @@ class FreqtradeBot(LoggingMixin):
# Updating open orders in dry-run does not make sense and will fail. # Updating open orders in dry-run does not make sense and will fail.
return return
trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees() trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
for trade in trades: for trade in trades:
if not trade.is_open and not trade.fee_updated('sell'): if not trade.is_open and not trade.fee_updated('sell'):

View File

@ -48,6 +48,13 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
sell_reason = get_column_def(cols, 'sell_reason', 'null') sell_reason = get_column_def(cols, 'sell_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null') strategy = get_column_def(cols, 'strategy', 'null')
buy_tag = get_column_def(cols, 'buy_tag', 'null') buy_tag = get_column_def(cols, 'buy_tag', 'null')
leverage = get_column_def(cols, 'leverage', '1.0')
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
isolated_liq = get_column_def(cols, 'isolated_liq', 'null')
# sqlite does not support literals for booleans
is_short = get_column_def(cols, 'is_short', '0')
interest_mode = get_column_def(cols, 'interest_mode', 'null')
# If ticker-interval existed use that, else null. # If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'): if has_column(cols, 'ticker_interval'):
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
@ -59,6 +66,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
close_profit_abs = get_column_def( close_profit_abs = get_column_def(
cols, 'close_profit_abs', cols, 'close_profit_abs',
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}")
# TODO-mg: update to exit order status
sell_order_status = get_column_def(cols, 'sell_order_status', 'null') sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
amount_requested = get_column_def(cols, 'amount_requested', 'amount') amount_requested = get_column_def(cols, 'amount_requested', 'amount')
@ -83,7 +91,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update, stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag,
timeframe, open_trade_value, close_profit_abs timeframe, open_trade_value, close_profit_abs,
leverage, interest_rate, isolated_liq, is_short, interest_mode
) )
select id, lower(exchange), pair, select id, lower(exchange), pair,
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
@ -99,7 +108,10 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
{sell_order_status} sell_order_status, {sell_order_status} sell_order_status,
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe, {strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
{leverage} leverage, {interest_rate} interest_rate,
{isolated_liq} isolated_liq, {is_short} is_short,
{interest_mode} interest_mode
from {table_back_name} from {table_back_name}
""")) """))
@ -134,14 +146,16 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col
# let SQLAlchemy create the schema as required # let SQLAlchemy create the schema as required
decl_base.metadata.create_all(engine) decl_base.metadata.create_all(engine)
leverage = get_column_def(cols, 'leverage', '1.0')
# sqlite does not support literals for booleans
with engine.begin() as connection: with engine.begin() as connection:
connection.execute(text(f""" connection.execute(text(f"""
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, average, remaining, cost, status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
order_date, order_filled_date, order_update_date) order_date, order_filled_date, order_update_date, leverage)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, null average, remaining, cost, status, symbol, order_type, side, price, amount, filled, null average, remaining, cost,
order_date, order_filled_date, order_update_date order_date, order_filled_date, order_update_date, {leverage} leverage
from {table_back_name} from {table_back_name}
""")) """))
@ -157,7 +171,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
table_back_name = get_backup_name(tabs, 'trades_bak') table_back_name = get_backup_name(tabs, 'trades_bak')
# Check for latest column # Check for latest column
if not has_column(cols, 'buy_tag'): if not has_column(cols, 'is_short'):
logger.info(f'Running database migration for trades - backup: {table_back_name}') logger.info(f'Running database migration for trades - backup: {table_back_name}')
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
# Reread columns - the above recreated the table! # Reread columns - the above recreated the table!
@ -170,9 +184,11 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
else: else:
cols_order = inspector.get_columns('orders') cols_order = inspector.get_columns('orders')
if not has_column(cols_order, 'average'): # Last added column of order table
# To determine if migrations need to run
if not has_column(cols_order, 'leverage'):
tabs = get_table_names_for_table(inspector, 'orders') tabs = get_table_names_for_table(inspector, 'orders')
# Empty for now - as there is only one iteration of the orders table so far. # Empty for now - as there is only one iteration of the orders table so far.
table_back_name = get_backup_name(tabs, 'orders_bak') table_back_name = get_backup_name(tabs, 'orders_bak')
migrate_orders_table(decl_base, inspector, engine, table_back_name, cols) migrate_orders_table(decl_base, inspector, engine, table_back_name, cols_order)

View File

@ -6,7 +6,7 @@ from datetime import datetime, timezone
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
create_engine, desc, func, inspect) create_engine, desc, func, inspect)
from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker
@ -14,7 +14,7 @@ from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.enums import SellType from freqtrade.enums import InterestMode, SellType
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import safe_value_fallback from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.migrations import check_migrate
@ -29,13 +29,13 @@ _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database
def init_db(db_url: str, clean_open_orders: bool = False) -> None: def init_db(db_url: str, clean_open_orders: bool = False) -> None:
""" """
Initializes this module with the given config, Initializes this module with the given config,
registers all known command handlers registers all known command handlers
and starts polling for message updates and starts polling for message updates
:param db_url: Database to use :param db_url: Database to use
:param clean_open_orders: Remove open orders from the database. :param clean_open_orders: Remove open orders from the database.
Useful for dry-run or if all orders have been reset on the exchange. Useful for dry-run or if all orders have been reset on the exchange.
:return: None :return: None
""" """
kwargs = {} kwargs = {}
@ -132,6 +132,8 @@ class Order(_DECL_BASE):
order_filled_date = Column(DateTime, nullable=True) order_filled_date = Column(DateTime, nullable=True)
order_update_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True)
leverage = Column(Float, nullable=True, default=1.0)
def __repr__(self): def __repr__(self):
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
@ -155,6 +157,8 @@ class Order(_DECL_BASE):
self.average = order.get('average', self.average) self.average = order.get('average', self.average)
self.remaining = order.get('remaining', self.remaining) self.remaining = order.get('remaining', self.remaining)
self.cost = order.get('cost', self.cost) self.cost = order.get('cost', self.cost)
self.leverage = order.get('leverage', self.leverage)
if 'timestamp' in order and order['timestamp'] is not None: if 'timestamp' in order and order['timestamp'] is not None:
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
@ -232,7 +236,7 @@ class LocalTrade():
close_rate_requested: Optional[float] = None close_rate_requested: Optional[float] = None
close_profit: Optional[float] = None close_profit: Optional[float] = None
close_profit_abs: Optional[float] = None close_profit_abs: Optional[float] = None
stake_amount: float = 0.0 stake_amount: float = 0.0 # TODO: This should probably be computed
amount: float = 0.0 amount: float = 0.0
amount_requested: Optional[float] = None amount_requested: Optional[float] = None
open_date: datetime open_date: datetime
@ -260,16 +264,31 @@ class LocalTrade():
buy_tag: Optional[str] = None buy_tag: Optional[str] = None
timeframe: Optional[int] = None timeframe: Optional[int] = None
def __init__(self, **kwargs): # Margin trading properties
for key in kwargs: interest_rate: float = 0.0
setattr(self, key, kwargs[key]) isolated_liq: Optional[float] = None
self.recalc_open_trade_value() is_short: bool = False
leverage: float = 1.0
interest_mode: InterestMode = InterestMode.NONE
def __repr__(self): @property
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' def has_no_leverage(self) -> bool:
"""Returns true if this is a non-leverage, non-short trade"""
return ((self.leverage or self.leverage is None) == 1.0 and not self.is_short)
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' @property
f'open_rate={self.open_rate:.8f}, open_since={open_since})') def borrowed(self) -> float:
"""
The amount of currency borrowed from the exchange for leverage trades
If a long trade, the amount is in base currency
If a short trade, the amount is in the other currency being traded
"""
if self.has_no_leverage:
return 0.0
elif not self.is_short:
return (self.amount * self.open_rate) * ((self.leverage-1)/self.leverage)
else:
return self.amount
@property @property
def open_date_utc(self): def open_date_utc(self):
@ -279,6 +298,77 @@ class LocalTrade():
def close_date_utc(self): def close_date_utc(self):
return self.close_date.replace(tzinfo=timezone.utc) return self.close_date.replace(tzinfo=timezone.utc)
@property
def enter_side(self) -> str:
if self.is_short:
return "sell"
else:
return "buy"
@property
def exit_side(self) -> str:
if self.is_short:
return "buy"
else:
return "sell"
def __init__(self, **kwargs):
for key in kwargs:
setattr(self, key, kwargs[key])
if self.isolated_liq:
self.set_isolated_liq(self.isolated_liq)
self.recalc_open_trade_value()
def _set_stop_loss(self, stop_loss: float, percent: float):
"""
Method you should use to set self.stop_loss.
Assures stop_loss is not passed the liquidation price
"""
if self.isolated_liq is not None:
if self.is_short:
sl = min(stop_loss, self.isolated_liq)
else:
sl = max(stop_loss, self.isolated_liq)
else:
sl = stop_loss
if not self.stop_loss:
self.initial_stop_loss = sl
self.stop_loss = sl
if self.is_short:
self.stop_loss_pct = abs(percent)
else:
self.stop_loss_pct = -1 * abs(percent)
self.stoploss_last_update = datetime.utcnow()
def set_isolated_liq(self, isolated_liq: float):
"""
Method you should use to set self.liquidation price.
Assures stop_loss is not passed the liquidation price
"""
if self.stop_loss is not None:
if self.is_short:
self.stop_loss = min(self.stop_loss, isolated_liq)
else:
self.stop_loss = max(self.stop_loss, isolated_liq)
else:
self.initial_stop_loss = isolated_liq
self.stop_loss = isolated_liq
self.isolated_liq = isolated_liq
def __repr__(self):
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
leverage = self.leverage or 1.0
is_short = self.is_short or False
return (
f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'is_short={is_short}, leverage={leverage}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})'
)
def to_json(self) -> Dict[str, Any]: def to_json(self) -> Dict[str, Any]:
return { return {
'trade_id': self.id, 'trade_id': self.id,
@ -342,6 +432,11 @@ class LocalTrade():
'min_rate': self.min_rate, 'min_rate': self.min_rate,
'max_rate': self.max_rate, 'max_rate': self.max_rate,
'leverage': self.leverage,
'interest_rate': self.interest_rate,
'isolated_liq': self.isolated_liq,
'is_short': self.is_short,
'open_order_id': self.open_order_id, 'open_order_id': self.open_order_id,
} }
@ -361,12 +456,6 @@ class LocalTrade():
self.max_rate = max(current_price, self.max_rate or self.open_rate) self.max_rate = max(current_price, self.max_rate or self.open_rate)
self.min_rate = min(current_price, self.min_rate or self.open_rate) self.min_rate = min(current_price, self.min_rate or self.open_rate)
def _set_new_stoploss(self, new_loss: float, stoploss: float):
"""Assign new stop value"""
self.stop_loss = new_loss
self.stop_loss_pct = -1 * abs(stoploss)
self.stoploss_last_update = datetime.utcnow()
def adjust_stop_loss(self, current_price: float, stoploss: float, def adjust_stop_loss(self, current_price: float, stoploss: float,
initial: bool = False) -> None: initial: bool = False) -> None:
""" """
@ -380,20 +469,39 @@ class LocalTrade():
# Don't modify if called with initial and nothing to do # Don't modify if called with initial and nothing to do
return return
new_loss = float(current_price * (1 - abs(stoploss))) if self.is_short:
new_loss = float(current_price * (1 + abs(stoploss)))
# If trading on margin, don't set the stoploss below the liquidation price
if self.isolated_liq:
new_loss = min(self.isolated_liq, new_loss)
else:
new_loss = float(current_price * (1 - abs(stoploss)))
# If trading on margin, don't set the stoploss below the liquidation price
if self.isolated_liq:
new_loss = max(self.isolated_liq, new_loss)
# no stop loss assigned yet # no stop loss assigned yet
if not self.stop_loss: if not self.stop_loss:
logger.debug(f"{self.pair} - Assigning new stoploss...") logger.debug(f"{self.pair} - Assigning new stoploss...")
self._set_new_stoploss(new_loss, stoploss) self._set_stop_loss(new_loss, stoploss)
self.initial_stop_loss = new_loss self.initial_stop_loss = new_loss
self.initial_stop_loss_pct = -1 * abs(stoploss) if self.is_short:
self.initial_stop_loss_pct = abs(stoploss)
else:
self.initial_stop_loss_pct = -1 * abs(stoploss)
# evaluate if the stop loss needs to be updated # evaluate if the stop loss needs to be updated
else: else:
if new_loss > self.stop_loss: # stop losses only walk up, never down!
higher_stop = new_loss > self.stop_loss
lower_stop = new_loss < self.stop_loss
# stop losses only walk up, never down!,
# ? But adding more to a margin account would create a lower liquidation price,
# ? decreasing the minimum stoploss
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
logger.debug(f"{self.pair} - Adjusting stoploss...") logger.debug(f"{self.pair} - Adjusting stoploss...")
self._set_new_stoploss(new_loss, stoploss) self._set_stop_loss(new_loss, stoploss)
else: else:
logger.debug(f"{self.pair} - Keeping current stoploss...") logger.debug(f"{self.pair} - Keeping current stoploss...")
@ -412,24 +520,35 @@ class LocalTrade():
:return: None :return: None
""" """
order_type = order['type'] order_type = order['type']
if 'is_short' in order and order['side'] == 'sell':
# Only set's is_short on opening trades, ignores non-shorts
self.is_short = order['is_short']
# Ignore open and cancelled orders # Ignore open and cancelled orders
if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None: if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None:
return return
logger.info('Updating trade (id=%s) ...', self.id) logger.info('Updating trade (id=%s) ...', self.id)
if order_type in ('market', 'limit') and order['side'] == 'buy': if order_type in ('market', 'limit') and self.enter_side == order['side']:
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
if 'leverage' in order:
self.leverage = order['leverage']
self.recalc_open_trade_value() self.recalc_open_trade_value()
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') payment = "SELL" if self.is_short else "BUY"
logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.')
self.open_order_id = None self.open_order_id = None
elif order_type in ('market', 'limit') and order['side'] == 'sell': elif order_type in ('market', 'limit') and self.exit_side == order['side']:
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') payment = "BUY" if self.is_short else "SELL"
self.close(safe_value_fallback(order, 'average', 'price')) # TODO-mg: On shorts, you buy a little bit more than the amount (amount + interest)
# This wll only print the original amount
logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.')
self.close(safe_value_fallback(order, 'average', 'price')) # TODO-mg: Double check this
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'):
self.stoploss_order_id = None self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
@ -447,9 +566,9 @@ class LocalTrade():
and marks trade as closed and marks trade as closed
""" """
self.close_rate = rate self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow()
self.close_profit = self.calc_profit_ratio() self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit() self.close_profit_abs = self.calc_profit()
self.close_date = self.close_date or datetime.utcnow()
self.is_open = False self.is_open = False
self.sell_order_status = 'closed' self.sell_order_status = 'closed'
self.open_order_id = None self.open_order_id = None
@ -464,14 +583,14 @@ class LocalTrade():
""" """
Update Fee parameters. Only acts once per side Update Fee parameters. Only acts once per side
""" """
if side == 'buy' and self.fee_open_currency is None: if self.enter_side == side and self.fee_open_currency is None:
self.fee_open_cost = fee_cost self.fee_open_cost = fee_cost
self.fee_open_currency = fee_currency self.fee_open_currency = fee_currency
if fee_rate is not None: if fee_rate is not None:
self.fee_open = fee_rate self.fee_open = fee_rate
# Assume close-fee will fall into the same fee category and take an educated guess # Assume close-fee will fall into the same fee category and take an educated guess
self.fee_close = fee_rate self.fee_close = fee_rate
elif side == 'sell' and self.fee_close_currency is None: elif self.exit_side == side and self.fee_close_currency is None:
self.fee_close_cost = fee_cost self.fee_close_cost = fee_cost
self.fee_close_currency = fee_currency self.fee_close_currency = fee_currency
if fee_rate is not None: if fee_rate is not None:
@ -481,9 +600,9 @@ class LocalTrade():
""" """
Verify if this side (buy / sell) has already been updated Verify if this side (buy / sell) has already been updated
""" """
if side == 'buy': if self.enter_side == side:
return self.fee_open_currency is not None return self.fee_open_currency is not None
elif side == 'sell': elif self.exit_side == side:
return self.fee_close_currency is not None return self.fee_close_currency is not None
else: else:
return False return False
@ -496,67 +615,129 @@ class LocalTrade():
Calculate the open_rate including open_fee. Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees :return: Price in of the open trade incl. Fees
""" """
buy_trade = Decimal(self.amount) * Decimal(self.open_rate) open_trade = Decimal(self.amount) * Decimal(self.open_rate)
fees = buy_trade * Decimal(self.fee_open) fees = open_trade * Decimal(self.fee_open)
return float(buy_trade + fees) if self.is_short:
return float(open_trade - fees)
else:
return float(open_trade + fees)
def recalc_open_trade_value(self) -> None: def recalc_open_trade_value(self) -> None:
""" """
Recalculate open_trade_value. Recalculate open_trade_value.
Must be called whenever open_rate or fee_open is changed. Must be called whenever open_rate, fee_open or is_short is changed.
""" """
self.open_trade_value = self._calc_open_trade_value() self.open_trade_value = self._calc_open_trade_value()
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal:
"""
: param interest_rate: interest_charge for borrowing this coin(optional).
If interest_rate is not set self.interest_rate will be used
"""
zero = Decimal(0.0)
# If nothing was borrowed
if self.has_no_leverage:
return zero
open_date = self.open_date.replace(tzinfo=None)
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
sec_per_hour = Decimal(3600)
total_seconds = Decimal((now - open_date).total_seconds())
hours = total_seconds/sec_per_hour or zero
rate = Decimal(interest_rate or self.interest_rate)
borrowed = Decimal(self.borrowed)
return self.interest_mode(borrowed=borrowed, rate=rate, hours=hours)
def calc_close_trade_value(self, rate: Optional[float] = None, def calc_close_trade_value(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float: fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
""" """
Calculate the close_rate including fee Calculate the close_rate including fee
:param fee: fee to use on the close rate (optional). :param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used If rate is not set self.fee will be used
:param rate: rate to compare with (optional). :param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: Price in BTC of the open trade :return: Price in BTC of the open trade
""" """
if rate is None and not self.close_rate: if rate is None and not self.close_rate:
return 0.0 return 0.0
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore interest = self.calculate_interest(interest_rate)
fees = sell_trade * Decimal(fee or self.fee_close) if self.is_short:
return float(sell_trade - fees) amount = Decimal(self.amount) + Decimal(interest)
else:
# Currency already owned for longs, no need to purchase
amount = Decimal(self.amount)
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
if self.is_short:
return float(close_trade + fees)
else:
return float(close_trade - fees - interest)
def calc_profit(self, rate: Optional[float] = None, def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float: fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
""" """
Calculate the absolute profit in stake currency between Close and Open trade Calculate the absolute profit in stake currency between Close and Open trade
:param fee: fee to use on the close rate (optional). :param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used If fee is not set self.fee will be used
:param rate: close rate to compare with (optional). :param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit in stake currency as float :return: profit in stake currency as float
""" """
close_trade_value = self.calc_close_trade_value( close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate), rate=(rate or self.close_rate),
fee=(fee or self.fee_close) fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
) )
profit = close_trade_value - self.open_trade_value
if self.is_short:
profit = self.open_trade_value - close_trade_value
else:
profit = close_trade_value - self.open_trade_value
return float(f"{profit:.8f}") return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: Optional[float] = None, def calc_profit_ratio(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float: fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
""" """
Calculates the profit as ratio (including fee). Calculates the profit as ratio (including fee).
:param rate: rate to compare with (optional). :param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional). :param fee: fee to use on the close rate (optional).
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit ratio as float :return: profit ratio as float
""" """
close_trade_value = self.calc_close_trade_value( close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate), rate=(rate or self.close_rate),
fee=(fee or self.fee_close) fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
) )
if self.open_trade_value == 0.0:
short_close_zero = (self.is_short and close_trade_value == 0.0)
long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
leverage = self.leverage or 1.0
if (short_close_zero or long_close_zero):
return 0.0 return 0.0
profit_ratio = (close_trade_value / self.open_trade_value) - 1 else:
if self.is_short:
profit_ratio = (1 - (close_trade_value/self.open_trade_value)) * leverage
else:
profit_ratio = ((close_trade_value/self.open_trade_value) - 1) * leverage
return float(f"{profit_ratio:.8f}") return float(f"{profit_ratio:.8f}")
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
@ -702,12 +883,20 @@ class Trade(_DECL_BASE, LocalTrade):
max_rate = Column(Float, nullable=True, default=0.0) max_rate = Column(Float, nullable=True, default=0.0)
# Lowest price reached # Lowest price reached
min_rate = Column(Float, nullable=True) min_rate = Column(Float, nullable=True)
sell_reason = Column(String(100), nullable=True) sell_reason = Column(String(100), nullable=True) # TODO-mg: Change to close_reason
sell_order_status = Column(String(100), nullable=True) sell_order_status = Column(String(100), nullable=True) # TODO-mg: Change to close_order_status
strategy = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True)
buy_tag = Column(String(100), nullable=True) buy_tag = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True) timeframe = Column(Integer, nullable=True)
# Margin trading properties
leverage = Column(Float, nullable=True, default=1.0)
interest_rate = Column(Float, nullable=False, default=0.0)
isolated_liq = Column(Float, nullable=True)
is_short = Column(Boolean, nullable=False, default=False)
interest_mode = Column(Enum(InterestMode), nullable=True)
# End of margin trading properties
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.recalc_open_trade_value() self.recalc_open_trade_value()
@ -794,7 +983,7 @@ class Trade(_DECL_BASE, LocalTrade):
]).all() ]).all()
@staticmethod @staticmethod
def get_sold_trades_without_assigned_fees(): def get_closed_trades_without_assigned_fees():
""" """
Returns all closed trades which don't have fees set correctly Returns all closed trades which don't have fees set correctly
NOTE: Not supported in Backtesting. NOTE: Not supported in Backtesting.

View File

@ -3,61 +3,62 @@ site_url: https://www.freqtrade.io/
repo_url: https://github.com/freqtrade/freqtrade repo_url: https://github.com/freqtrade/freqtrade
use_directory_urls: True use_directory_urls: True
nav: nav:
- Home: index.md - Home: index.md
- Quickstart with Docker: docker_quickstart.md - Quickstart with Docker: docker_quickstart.md
- Installation: - Installation:
- Linux/MacOS/Raspberry: installation.md - Linux/MacOS/Raspberry: installation.md
- Windows: windows_installation.md - Windows: windows_installation.md
- Freqtrade Basics: bot-basics.md - Freqtrade Basics: bot-basics.md
- Configuration: configuration.md - Configuration: configuration.md
- Strategy Customization: strategy-customization.md - Strategy Customization: strategy-customization.md
- Plugins: plugins.md - Plugins: plugins.md
- Stoploss: stoploss.md - Stoploss: stoploss.md
- Start the bot: bot-usage.md - Start the bot: bot-usage.md
- Control the bot: - Control the bot:
- Telegram: telegram-usage.md - Telegram: telegram-usage.md
- REST API & FreqUI: rest-api.md - REST API & FreqUI: rest-api.md
- Web Hook: webhook-config.md - Web Hook: webhook-config.md
- Data Downloading: data-download.md - Data Downloading: data-download.md
- Backtesting: backtesting.md - Backtesting: backtesting.md
- Hyperopt: hyperopt.md - Leverage: leverage.md
- Utility Sub-commands: utils.md - Hyperopt: hyperopt.md
- Plotting: plotting.md - Utility Sub-commands: utils.md
- Data Analysis: - Plotting: plotting.md
- Jupyter Notebooks: data-analysis.md - Data Analysis:
- Strategy analysis: strategy_analysis_example.md - Jupyter Notebooks: data-analysis.md
- Exchange-specific Notes: exchanges.md - Strategy analysis: strategy_analysis_example.md
- Advanced Topics: - Exchange-specific Notes: exchanges.md
- Advanced Post-installation Tasks: advanced-setup.md - Advanced Topics:
- Edge Positioning: edge.md - Advanced Post-installation Tasks: advanced-setup.md
- Advanced Strategy: strategy-advanced.md - Edge Positioning: edge.md
- Advanced Hyperopt: advanced-hyperopt.md - Advanced Strategy: strategy-advanced.md
- Sandbox Testing: sandbox-testing.md - Advanced Hyperopt: advanced-hyperopt.md
- FAQ: faq.md - Sandbox Testing: sandbox-testing.md
- SQL Cheat-sheet: sql_cheatsheet.md - FAQ: faq.md
- Updating Freqtrade: updating.md - SQL Cheat-sheet: sql_cheatsheet.md
- Deprecated Features: deprecated.md - Updating Freqtrade: updating.md
- Contributors Guide: developer.md - Deprecated Features: deprecated.md
- Contributors Guide: developer.md
theme: theme:
name: material name: material
logo: 'images/logo.png' logo: "images/logo.png"
favicon: 'images/logo.png' favicon: "images/logo.png"
custom_dir: 'docs/overrides' custom_dir: "docs/overrides"
palette: palette:
- scheme: default - scheme: default
primary: 'blue grey' primary: "blue grey"
accent: 'tear' accent: "tear"
toggle: toggle:
icon: material/toggle-switch-off-outline icon: material/toggle-switch-off-outline
name: Switch to dark mode name: Switch to dark mode
- scheme: slate - scheme: slate
primary: 'blue grey' primary: "blue grey"
accent: 'tear' accent: "tear"
toggle: toggle:
icon: material/toggle-switch-off-outline icon: material/toggle-switch-off-outline
name: Switch to dark mode name: Switch to dark mode
extra_css: extra_css:
- 'stylesheets/ft.extra.css' - "stylesheets/ft.extra.css"
extra_javascript: extra_javascript:
- javascripts/config.js - javascripts/config.js
- https://polyfill.io/v3/polyfill.min.js?features=es6 - https://polyfill.io/v3/polyfill.min.js?features=es6

View File

@ -23,8 +23,8 @@ from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.persistence import LocalTrade, Trade, init_db
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.worker import Worker from freqtrade.worker import Worker
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, from tests.conftest_trades import (leverage_trade, mock_trade_1, mock_trade_2, mock_trade_3,
mock_trade_5, mock_trade_6) mock_trade_4, mock_trade_5, mock_trade_6, short_trade)
logging.getLogger('').setLevel(logging.INFO) logging.getLogger('').setLevel(logging.INFO)
@ -225,6 +225,43 @@ def create_mock_trades(fee, use_db: bool = True):
Trade.query.session.flush() Trade.query.session.flush()
def create_mock_trades_with_leverage(fee, use_db: bool = True):
"""
Create some fake trades ...
"""
def add_trade(trade):
if use_db:
Trade.query.session.add(trade)
else:
LocalTrade.add_bt_trade(trade)
# Simulate dry_run entries
trade = mock_trade_1(fee)
add_trade(trade)
trade = mock_trade_2(fee)
add_trade(trade)
trade = mock_trade_3(fee)
add_trade(trade)
trade = mock_trade_4(fee)
add_trade(trade)
trade = mock_trade_5(fee)
add_trade(trade)
trade = mock_trade_6(fee)
add_trade(trade)
trade = short_trade(fee)
add_trade(trade)
trade = leverage_trade(fee)
add_trade(trade)
if use_db:
Trade.query.session.flush()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def patch_coingekko(mocker) -> None: def patch_coingekko(mocker) -> None:
""" """

View File

@ -1,5 +1,6 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from freqtrade.enums import InterestMode
from freqtrade.persistence.models import Order, Trade from freqtrade.persistence.models import Order, Trade
@ -303,3 +304,180 @@ def mock_trade_6(fee):
o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell') o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell')
trade.orders.append(o) trade.orders.append(o)
return trade return trade
def short_order():
return {
'id': '1236',
'symbol': 'ETC/BTC',
'status': 'closed',
'side': 'sell',
'type': 'limit',
'price': 0.123,
'amount': 123.0,
'filled': 123.0,
'remaining': 0.0,
}
def exit_short_order():
return {
'id': '12367',
'symbol': 'ETC/BTC',
'status': 'closed',
'side': 'buy',
'type': 'limit',
'price': 0.128,
'amount': 123.0,
'filled': 123.0,
'remaining': 0.0,
}
def short_trade(fee):
"""
10 minute short limit trade on binance
Short trade
fee: 0.25% base
interest_rate: 0.05% per day
open_rate: 0.123 base
close_rate: 0.128 base
amount: 123.0 crypto
stake_amount: 15.129 base
borrowed: 123.0 crypto
time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day)
interest: borrowed * interest_rate * time-periods
= 123.0 * 0.0005 * 1/24 = 0.0025625 crypto
open_value: (amount * open_rate) - (amount * open_rate * fee)
= (123 * 0.123) - (123 * 0.123 * 0.0025)
= 15.091177499999999
amount_closed: amount + interest = 123 + 0.0025625 = 123.0025625
close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee)
= (123.0025625 * 0.128) + (123.0025625 * 0.128 * 0.0025)
= 15.78368882
total_profit = open_value - close_value
= 15.091177499999999 - 15.78368882
= -0.6925113200000013
total_profit_percentage = total_profit / stake_amount
= -0.6925113200000013 / 15.129
= -0.04577376693766946
"""
trade = Trade(
pair='ETC/BTC',
stake_amount=15.129,
amount=123.0,
amount_requested=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
# close_rate=0.128,
# close_profit=-0.04577376693766946,
# close_profit_abs=-0.6925113200000013,
exchange='binance',
is_open=True,
open_order_id='dry_run_exit_short_12345',
strategy='DefaultStrategy',
timeframe=5,
sell_reason='sell_signal', # TODO-mg: Update to exit/close reason
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
# close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
is_short=True,
interest_mode=InterestMode.HOURSPERDAY
)
o = Order.parse_from_ccxt_object(short_order(), 'ETC/BTC', 'sell')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(exit_short_order(), 'ETC/BTC', 'sell')
trade.orders.append(o)
return trade
def leverage_order():
return {
'id': '1237',
'symbol': 'DOGE/BTC',
'status': 'closed',
'side': 'buy',
'type': 'limit',
'price': 0.123,
'amount': 123.0,
'filled': 123.0,
'remaining': 0.0,
'leverage': 5.0
}
def leverage_order_sell():
return {
'id': '12368',
'symbol': 'DOGE/BTC',
'status': 'closed',
'side': 'sell',
'type': 'limit',
'price': 0.128,
'amount': 123.0,
'filled': 123.0,
'remaining': 0.0,
'leverage': 5.0
}
def leverage_trade(fee):
"""
5 hour short limit trade on kraken
Short trade
fee: 0.25% base
interest_rate: 0.05% per day
open_rate: 0.123 base
close_rate: 0.128 base
amount: 615 crypto
stake_amount: 15.129 base
borrowed: 60.516 base
leverage: 5
hours: 5
interest: borrowed * interest_rate * ceil(1 + hours/4)
= 60.516 * 0.0005 * ceil(1 + 5/4) = 0.090774 base
open_value: (amount * open_rate) + (amount * open_rate * fee)
= (615.0 * 0.123) + (615.0 * 0.123 * 0.0025)
= 75.83411249999999
close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest
= (615.0 * 0.128) - (615.0 * 0.128 * 0.0025) - 0.090774
= 78.432426
total_profit = close_value - open_value
= 78.432426 - 75.83411249999999
= 2.5983135000000175
total_profit_percentage = ((close_value/open_value)-1) * leverage
= ((78.432426/75.83411249999999)-1) * 5
= 0.1713156134055116
"""
trade = Trade(
pair='DOGE/BTC',
stake_amount=15.129,
amount=615.0,
leverage=5.0,
amount_requested=615.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
close_rate=0.128,
close_profit=0.1713156134055116,
close_profit_abs=2.5983135000000175,
exchange='kraken',
is_open=False,
open_order_id='dry_run_leverage_buy_12368',
strategy='DefaultStrategy',
timeframe=5,
sell_reason='sell_signal', # TODO-mg: Update to exit/close reason
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=300),
close_date=datetime.now(tz=timezone.utc),
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPER4
)
o = Order.parse_from_ccxt_object(leverage_order(), 'DOGE/BTC', 'sell')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(leverage_order_sell(), 'DOGE/BTC', 'sell')
trade.orders.append(o)
return trade

View File

@ -108,6 +108,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'leverage': 1.0,
'interest_rate': 0.0,
'isolated_liq': None,
'is_short': False,
} }
mocker.patch('freqtrade.exchange.Exchange.get_rate', mocker.patch('freqtrade.exchange.Exchange.get_rate',
@ -175,6 +179,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'leverage': 1.0,
'interest_rate': 0.0,
'isolated_liq': None,
'is_short': False,
} }

View File

@ -2434,6 +2434,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert log_has_re(r"Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, " assert log_has_re(r"Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, "
r"is_short=False, leverage=1.0, "
r"open_rate=0.00001099, open_since=" r"open_rate=0.00001099, open_since="
f"{open_trade.open_date.strftime('%Y-%m-%d %H:%M:%S')}" f"{open_trade.open_date.strftime('%Y-%m-%d %H:%M:%S')}"
r"\) due to Traceback \(most recent call last\):\n*", r"\) due to Traceback \(most recent call last\):\n*",
@ -3619,9 +3620,11 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fe
# 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(
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).', 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,'
caplog) ' leverage=1.0, 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, def test_get_real_amount_quote_dust(default_conf, trades_for_order, buy_order_fee, fee,
@ -3666,9 +3669,12 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f
# 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
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' assert log_has(
'open_rate=0.24544100, open_since=closed) failed: myTrade-Dict empty found', 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
caplog) 'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) failed: '
'myTrade-Dict empty found',
caplog
)
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):
@ -3752,9 +3758,11 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c
# 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(
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).', 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,'
caplog) ' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
caplog
)
assert trade.fee_open == 0.001 assert trade.fee_open == 0.001
assert trade.fee_close == 0.001 assert trade.fee_close == 0.001
@ -3788,9 +3796,11 @@ def test_get_real_amount_multi2(default_conf, trades_for_order3, buy_order_fee,
# Amount is reduced by "fee" # Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.0005) assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.0005)
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' assert log_has(
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).', 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,'
caplog) ' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).',
caplog
)
# Overall fee is average of both trade's fee # Overall fee is average of both trade's fee
assert trade.fee_open == 0.001518575 assert trade.fee_open == 0.001518575
assert trade.fee_open_cost is not None assert trade.fee_open_cost is not None
@ -3822,9 +3832,11 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
# 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(
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).', 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,'
caplog) ' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).',
caplog
)
def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, fee, mocker): def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, fee, mocker):

File diff suppressed because it is too large Load Diff