merge leverage/margin
This commit is contained in:
@@ -74,9 +74,25 @@ def migrate_trades_and_orders_table(
|
||||
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
|
||||
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
||||
min_rate = get_column_def(cols, 'min_rate', 'null')
|
||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||
exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null'))
|
||||
strategy = get_column_def(cols, 'strategy', 'null')
|
||||
buy_tag = get_column_def(cols, 'buy_tag', 'null')
|
||||
enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null'))
|
||||
|
||||
trading_mode = get_column_def(cols, 'trading_mode', 'null')
|
||||
|
||||
# Leverage Properties
|
||||
leverage = get_column_def(cols, 'leverage', '1.0')
|
||||
liquidation_price = get_column_def(cols, 'liquidation_price',
|
||||
get_column_def(cols, 'isolated_liq', 'null'))
|
||||
# sqlite does not support literals for booleans
|
||||
is_short = get_column_def(cols, 'is_short', '0')
|
||||
|
||||
# Margin Properties
|
||||
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
|
||||
|
||||
# Futures properties
|
||||
funding_fees = get_column_def(cols, 'funding_fees', '0.0')
|
||||
|
||||
# If ticker-interval existed use that, else null.
|
||||
if has_column(cols, 'ticker_interval'):
|
||||
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
|
||||
@@ -88,7 +104,8 @@ def migrate_trades_and_orders_table(
|
||||
close_profit_abs = get_column_def(
|
||||
cols, 'close_profit_abs',
|
||||
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}")
|
||||
sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
|
||||
exit_order_status = get_column_def(cols, 'exit_order_status',
|
||||
get_column_def(cols, 'sell_order_status', 'null'))
|
||||
amount_requested = get_column_def(cols, 'amount_requested', 'amount')
|
||||
|
||||
# Schema migration necessary
|
||||
@@ -120,8 +137,10 @@ def migrate_trades_and_orders_table(
|
||||
stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
|
||||
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
||||
stoploss_order_id, stoploss_last_update,
|
||||
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag,
|
||||
timeframe, open_trade_value, close_profit_abs
|
||||
max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
|
||||
timeframe, open_trade_value, close_profit_abs,
|
||||
trading_mode, leverage, liquidation_price, is_short,
|
||||
interest_rate, funding_fees
|
||||
)
|
||||
select id, lower(exchange), pair,
|
||||
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
|
||||
@@ -134,10 +153,13 @@ def migrate_trades_and_orders_table(
|
||||
{initial_stop_loss} initial_stop_loss,
|
||||
{initial_stop_loss_pct} initial_stop_loss_pct,
|
||||
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
||||
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
||||
{sell_order_status} sell_order_status,
|
||||
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
|
||||
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
|
||||
{max_rate} max_rate, {min_rate} min_rate, {exit_reason} exit_reason,
|
||||
{exit_order_status} exit_order_status,
|
||||
{strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe,
|
||||
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
|
||||
{trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price,
|
||||
{is_short} is_short, {interest_rate} interest_rate,
|
||||
{funding_fees} funding_fees
|
||||
from {trade_back_name}
|
||||
"""))
|
||||
|
||||
@@ -176,12 +198,12 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List):
|
||||
ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
|
||||
average = get_column_def(cols_order, 'average', 'null')
|
||||
|
||||
# let SQLAlchemy create the schema as required
|
||||
# sqlite does not support literals for booleans
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(f"""
|
||||
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, order_date, order_filled_date, order_update_date, ft_fee_base)
|
||||
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
|
||||
order_date, order_filled_date, order_update_date, ft_fee_base)
|
||||
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
||||
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
|
||||
cost, order_date, order_filled_date, order_update_date, {ft_fee_base} ft_fee_base
|
||||
@@ -211,8 +233,9 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
|
||||
# Check if migration necessary
|
||||
# Migrates both trades and orders table!
|
||||
# if not has_column(cols, 'buy_tag'):
|
||||
if 'orders' not in previous_tables or not has_column(cols_orders, 'ft_fee_base'):
|
||||
# if ('orders' not in previous_tables
|
||||
# or not has_column(cols_orders, 'leverage')):
|
||||
if not has_column(cols, 'exit_order_status'):
|
||||
logger.info(f"Running database migration for trades - "
|
||||
f"backup: {table_back_name}, {order_table_bak_name}")
|
||||
migrate_trades_and_orders_table(
|
||||
|
||||
@@ -6,7 +6,7 @@ from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
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)
|
||||
from sqlalchemy.exc import NoSuchModuleError
|
||||
from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker
|
||||
@@ -14,8 +14,9 @@ from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy.sql.schema import UniqueConstraint
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
|
||||
from freqtrade.enums import SellType
|
||||
from freqtrade.enums import ExitType, TradingMode
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.leverage import interest
|
||||
from freqtrade.persistence.migrations import check_migrate
|
||||
|
||||
|
||||
@@ -181,6 +182,7 @@ class Order(_DECL_BASE):
|
||||
self.average = order.get('average', self.average)
|
||||
self.remaining = order.get('remaining', self.remaining)
|
||||
self.cost = order.get('cost', self.cost)
|
||||
|
||||
if 'timestamp' in order and order['timestamp'] is not None:
|
||||
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
|
||||
|
||||
@@ -191,7 +193,7 @@ class Order(_DECL_BASE):
|
||||
self.order_filled_date = datetime.now(timezone.utc)
|
||||
self.order_update_date = datetime.now(timezone.utc)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
def to_json(self, entry_side: str) -> Dict[str, Any]:
|
||||
return {
|
||||
'pair': self.ft_pair,
|
||||
'order_id': self.order_id,
|
||||
@@ -213,6 +215,7 @@ class Order(_DECL_BASE):
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||
'order_type': self.order_type,
|
||||
'price': self.price,
|
||||
'ft_is_entry': self.ft_order_side == entry_side,
|
||||
'remaining': self.remaining,
|
||||
}
|
||||
|
||||
@@ -314,22 +317,51 @@ class LocalTrade():
|
||||
max_rate: float = 0.0
|
||||
# Lowest price reached
|
||||
min_rate: float = 0.0
|
||||
sell_reason: str = ''
|
||||
sell_order_status: str = ''
|
||||
exit_reason: str = ''
|
||||
exit_order_status: str = ''
|
||||
strategy: str = ''
|
||||
buy_tag: Optional[str] = None
|
||||
enter_tag: Optional[str] = None
|
||||
timeframe: Optional[int] = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key in kwargs:
|
||||
setattr(self, key, kwargs[key])
|
||||
self.recalc_open_trade_value()
|
||||
trading_mode: TradingMode = TradingMode.SPOT
|
||||
|
||||
def __repr__(self):
|
||||
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
|
||||
# Leverage trading properties
|
||||
liquidation_price: Optional[float] = None
|
||||
is_short: bool = False
|
||||
leverage: float = 1.0
|
||||
|
||||
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
||||
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
|
||||
# Margin trading properties
|
||||
interest_rate: float = 0.0
|
||||
|
||||
# Futures properties
|
||||
funding_fees: Optional[float] = None
|
||||
|
||||
@property
|
||||
def buy_tag(self) -> Optional[str]:
|
||||
"""
|
||||
Compatibility between buy_tag (old) and enter_tag (new)
|
||||
Consider buy_tag deprecated
|
||||
"""
|
||||
return self.enter_tag
|
||||
|
||||
@property
|
||||
def has_no_leverage(self) -> bool:
|
||||
"""Returns true if this is a non-leverage, non-short trade"""
|
||||
return ((self.leverage == 1.0 or self.leverage is None) and not self.is_short)
|
||||
|
||||
@property
|
||||
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
|
||||
def open_date_utc(self):
|
||||
@@ -339,9 +371,49 @@ class LocalTrade():
|
||||
def close_date_utc(self):
|
||||
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"
|
||||
|
||||
@property
|
||||
def trade_direction(self) -> str:
|
||||
if self.is_short:
|
||||
return "short"
|
||||
else:
|
||||
return "long"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key in kwargs:
|
||||
setattr(self, key, kwargs[key])
|
||||
self.recalc_open_trade_value()
|
||||
if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None:
|
||||
raise OperationalException(
|
||||
f"{self.trading_mode.value} trading requires param interest_rate on trades")
|
||||
|
||||
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]:
|
||||
filled_orders = self.select_filled_orders()
|
||||
orders = [order.to_json() for order in filled_orders]
|
||||
orders = [order.to_json(self.enter_side) for order in filled_orders]
|
||||
|
||||
return {
|
||||
'trade_id': self.id,
|
||||
@@ -352,7 +424,8 @@ class LocalTrade():
|
||||
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
||||
'stake_amount': round(self.stake_amount, 8),
|
||||
'strategy': self.strategy,
|
||||
'buy_tag': self.buy_tag,
|
||||
'buy_tag': self.enter_tag,
|
||||
'enter_tag': self.enter_tag,
|
||||
'timeframe': self.timeframe,
|
||||
|
||||
'fee_open': self.fee_open,
|
||||
@@ -388,8 +461,9 @@ class LocalTrade():
|
||||
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
||||
'profit_abs': self.close_profit_abs,
|
||||
|
||||
'sell_reason': self.sell_reason,
|
||||
'sell_order_status': self.sell_order_status,
|
||||
'sell_reason': self.exit_reason, # Deprecated
|
||||
'exit_reason': self.exit_reason,
|
||||
'exit_order_status': self.exit_order_status,
|
||||
'stop_loss_abs': self.stop_loss,
|
||||
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
|
||||
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
||||
@@ -406,6 +480,12 @@ class LocalTrade():
|
||||
'min_rate': self.min_rate,
|
||||
'max_rate': self.max_rate,
|
||||
|
||||
'leverage': self.leverage,
|
||||
'interest_rate': self.interest_rate,
|
||||
'liquidation_price': self.liquidation_price,
|
||||
'is_short': self.is_short,
|
||||
'trading_mode': self.trading_mode,
|
||||
'funding_fees': self.funding_fees,
|
||||
'open_order_id': self.open_order_id,
|
||||
'orders': orders,
|
||||
}
|
||||
@@ -426,10 +506,33 @@ class LocalTrade():
|
||||
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
||||
self.min_rate = min(current_price_low, 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)
|
||||
def set_isolated_liq(self, liquidation_price: Optional[float]):
|
||||
"""
|
||||
Method you should use to set self.liquidation price.
|
||||
Assures stop_loss is not passed the liquidation price
|
||||
"""
|
||||
if not liquidation_price:
|
||||
return
|
||||
self.liquidation_price = liquidation_price
|
||||
|
||||
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.liquidation_price is not None:
|
||||
if self.is_short:
|
||||
sl = min(stop_loss, self.liquidation_price)
|
||||
else:
|
||||
sl = max(stop_loss, self.liquidation_price)
|
||||
else:
|
||||
sl = stop_loss
|
||||
|
||||
if not self.stop_loss:
|
||||
self.initial_stop_loss = sl
|
||||
self.stop_loss = sl
|
||||
|
||||
self.stop_loss_pct = -1 * abs(percent)
|
||||
self.stoploss_last_update = datetime.utcnow()
|
||||
|
||||
def adjust_stop_loss(self, current_price: float, stoploss: float,
|
||||
@@ -445,27 +548,43 @@ class LocalTrade():
|
||||
# Don't modify if called with initial and nothing to do
|
||||
return
|
||||
|
||||
new_loss = float(current_price * (1 - abs(stoploss)))
|
||||
leverage = self.leverage or 1.0
|
||||
if self.is_short:
|
||||
new_loss = float(current_price * (1 + abs(stoploss / leverage)))
|
||||
# If trading with leverage, don't set the stoploss below the liquidation price
|
||||
if self.liquidation_price:
|
||||
new_loss = min(self.liquidation_price, new_loss)
|
||||
else:
|
||||
new_loss = float(current_price * (1 - abs(stoploss / leverage)))
|
||||
# If trading with leverage, don't set the stoploss below the liquidation price
|
||||
if self.liquidation_price:
|
||||
new_loss = max(self.liquidation_price, new_loss)
|
||||
|
||||
# no stop loss assigned yet
|
||||
# if not self.stop_loss:
|
||||
if self.initial_stop_loss_pct is None:
|
||||
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_pct = -1 * abs(stoploss)
|
||||
|
||||
# evaluate if the stop loss needs to be updated
|
||||
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 leveraged trade 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...")
|
||||
self._set_new_stoploss(new_loss, stoploss)
|
||||
self._set_stop_loss(new_loss, stoploss)
|
||||
else:
|
||||
logger.debug(f"{self.pair} - Keeping current stoploss...")
|
||||
|
||||
logger.debug(
|
||||
f"{self.pair} - Stoploss adjusted. current_price={current_price:.8f}, "
|
||||
f"open_rate={self.open_rate:.8f}, max_rate={self.max_rate:.8f}, "
|
||||
f"open_rate={self.open_rate:.8f}, max_rate={self.max_rate or self.open_rate:.8f}, "
|
||||
f"initial_stop_loss={self.initial_stop_loss:.8f}, "
|
||||
f"stop_loss={self.stop_loss:.8f}. "
|
||||
f"Trailing stoploss saved us: "
|
||||
@@ -477,25 +596,29 @@ class LocalTrade():
|
||||
:param order: order retrieved by exchange.fetch_order()
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# Ignore open and cancelled orders
|
||||
if order.status == 'open' or order.safe_price is None:
|
||||
return
|
||||
|
||||
logger.info(f'Updating trade (id={self.id}) ...')
|
||||
|
||||
if order.ft_order_side == 'buy':
|
||||
if order.ft_order_side == self.enter_side:
|
||||
# Update open rate and actual amount
|
||||
self.open_rate = order.safe_price
|
||||
self.amount = order.safe_amount_after_fee
|
||||
if self.is_open:
|
||||
logger.info(f'{order.order_type.upper()}_BUY has been fulfilled for {self}.')
|
||||
payment = "SELL" if self.is_short else "BUY"
|
||||
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
|
||||
# condition to avoid reset value when updating fees
|
||||
if self.open_order_id == order.order_id:
|
||||
self.open_order_id = None
|
||||
self.recalc_trade_from_orders()
|
||||
elif order.ft_order_side == 'sell':
|
||||
elif order.ft_order_side == self.exit_side:
|
||||
if self.is_open:
|
||||
logger.info(f'{order.order_type.upper()}_SELL has been fulfilled for {self}.')
|
||||
payment = "BUY" if self.is_short else "SELL"
|
||||
# * On margin shorts, you buy a little bit more than the amount (amount + interest)
|
||||
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
|
||||
# condition to avoid reset value when updating fees
|
||||
if self.open_order_id == order.order_id:
|
||||
self.open_order_id = None
|
||||
@@ -506,7 +629,7 @@ class LocalTrade():
|
||||
elif order.ft_order_side == 'stoploss':
|
||||
self.stoploss_order_id = None
|
||||
self.close_rate_requested = self.stop_loss
|
||||
self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||
if self.is_open:
|
||||
logger.info(f'{order.order_type.upper()} is hit for {self}.')
|
||||
self.close(order.safe_price)
|
||||
@@ -540,11 +663,11 @@ class LocalTrade():
|
||||
and marks trade as closed
|
||||
"""
|
||||
self.close_rate = rate
|
||||
self.close_date = self.close_date or datetime.utcnow()
|
||||
self.close_profit = self.calc_profit_ratio()
|
||||
self.close_profit_abs = self.calc_profit() + self.realized_profit
|
||||
self.close_date = self.close_date or datetime.utcnow()
|
||||
self.is_open = False
|
||||
self.sell_order_status = 'closed'
|
||||
self.exit_order_status = 'closed'
|
||||
self.open_order_id = None
|
||||
if show_msg:
|
||||
logger.info(
|
||||
@@ -557,14 +680,14 @@ class LocalTrade():
|
||||
"""
|
||||
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_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:
|
||||
elif self.exit_side == side 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:
|
||||
@@ -574,9 +697,9 @@ class LocalTrade():
|
||||
"""
|
||||
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
|
||||
elif side == 'sell':
|
||||
elif self.exit_side == side:
|
||||
return self.fee_close_currency is not None
|
||||
else:
|
||||
return False
|
||||
@@ -589,74 +712,159 @@ class LocalTrade():
|
||||
Get amount of failed exiting orders
|
||||
assumes full exits.
|
||||
"""
|
||||
return len([o for o in self.orders if o.ft_order_side == 'sell'])
|
||||
return len([o for o in self.orders if o.ft_order_side == self.exit_side])
|
||||
|
||||
def _calc_open_trade_value(self) -> float:
|
||||
"""
|
||||
Calculate the open_rate including open_fee.
|
||||
:return: Price in of the open trade incl. Fees
|
||||
"""
|
||||
buy_trade = Decimal(self.amount) * Decimal(self.open_rate)
|
||||
fees = buy_trade * Decimal(self.fee_open)
|
||||
return float(buy_trade + fees)
|
||||
open_trade = Decimal(self.amount) * Decimal(self.open_rate)
|
||||
fees = open_trade * Decimal(self.fee_open)
|
||||
if self.is_short:
|
||||
return float(open_trade - fees)
|
||||
else:
|
||||
return float(open_trade + fees)
|
||||
|
||||
def recalc_open_trade_value(self) -> None:
|
||||
"""
|
||||
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()
|
||||
|
||||
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.trading_mode != TradingMode.MARGIN or 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 interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
|
||||
|
||||
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None) -> Decimal:
|
||||
|
||||
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 close_trade + fees
|
||||
else:
|
||||
return close_trade - fees
|
||||
|
||||
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
|
||||
:param fee: fee to use on the close rate (optional).
|
||||
If rate is not set self.fee will be used
|
||||
:param rate: rate to compare with (optional).
|
||||
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
|
||||
"""
|
||||
if rate is None and not self.close_rate:
|
||||
return 0.0
|
||||
|
||||
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore
|
||||
fees = sell_trade * Decimal(fee or self.fee_close)
|
||||
return float(sell_trade - fees)
|
||||
amount = Decimal(self.amount)
|
||||
trading_mode = self.trading_mode or TradingMode.SPOT
|
||||
|
||||
if trading_mode == TradingMode.SPOT:
|
||||
return float(self._calc_base_close(amount, rate, fee))
|
||||
|
||||
elif (trading_mode == TradingMode.MARGIN):
|
||||
|
||||
total_interest = self.calculate_interest(interest_rate)
|
||||
|
||||
if self.is_short:
|
||||
amount = amount + total_interest
|
||||
return float(self._calc_base_close(amount, rate, fee))
|
||||
else:
|
||||
# Currency already owned for longs, no need to purchase
|
||||
return float(self._calc_base_close(amount, rate, fee) - total_interest)
|
||||
|
||||
elif (trading_mode == TradingMode.FUTURES):
|
||||
funding_fees = self.funding_fees or 0.0
|
||||
# Positive funding_fees -> Trade has gained from fees.
|
||||
# Negative funding_fees -> Trade had to pay the fees.
|
||||
if self.is_short:
|
||||
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
|
||||
else:
|
||||
return float(self._calc_base_close(amount, rate, fee)) + funding_fees
|
||||
else:
|
||||
raise OperationalException(
|
||||
f"{self.trading_mode.value} trading is not yet available using freqtrade")
|
||||
|
||||
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
|
||||
: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).
|
||||
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
|
||||
"""
|
||||
close_trade_value = self.calc_close_trade_value(
|
||||
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}")
|
||||
|
||||
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).
|
||||
:param rate: rate to compare with (optional).
|
||||
If rate is not set self.close_rate will be used
|
||||
: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
|
||||
"""
|
||||
close_trade_value = self.calc_close_trade_value(
|
||||
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
|
||||
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}")
|
||||
|
||||
def recalc_trade_from_orders(self):
|
||||
@@ -680,8 +888,9 @@ class LocalTrade():
|
||||
avg_price = total_stake / total_amount
|
||||
|
||||
if total_amount > 0:
|
||||
# Leverage not updated, as we don't allow changing leverage through DCA at the moment.
|
||||
self.open_rate = total_stake / total_amount
|
||||
self.stake_amount = total_stake
|
||||
self.stake_amount = total_stake / (self.leverage or 1.0)
|
||||
self.amount = total_amount
|
||||
self.fee_open_cost = self.fee_open * self.stake_amount
|
||||
self.recalc_open_trade_value()
|
||||
@@ -727,10 +936,28 @@ class LocalTrade():
|
||||
(o.filled or 0) > 0 and
|
||||
o.status in NON_OPEN_EXCHANGE_STATES]
|
||||
|
||||
@property
|
||||
def nr_of_successful_entries(self) -> int:
|
||||
"""
|
||||
Helper function to count the number of entry orders that have been filled.
|
||||
:return: int count of entry orders that have been filled for this trade.
|
||||
"""
|
||||
|
||||
return len(self.select_filled_orders(self.enter_side))
|
||||
|
||||
@property
|
||||
def nr_of_successful_exits(self) -> int:
|
||||
"""
|
||||
Helper function to count the number of exit orders that have been filled.
|
||||
:return: int count of exit orders that have been filled for this trade.
|
||||
"""
|
||||
return len(self.select_filled_orders(self.exit_side))
|
||||
|
||||
@property
|
||||
def nr_of_successful_buys(self) -> int:
|
||||
"""
|
||||
Helper function to count the number of buy orders that have been filled.
|
||||
WARNING: Please use nr_of_successful_entries for short support.
|
||||
:return: int count of buy orders that have been filled for this trade.
|
||||
"""
|
||||
|
||||
@@ -740,10 +967,16 @@ class LocalTrade():
|
||||
def nr_of_successful_sells(self) -> int:
|
||||
"""
|
||||
Helper function to count the number of sell orders that have been filled.
|
||||
WARNING: Please use nr_of_successful_exits for short support.
|
||||
:return: int count of sell orders that have been filled for this trade.
|
||||
"""
|
||||
return len(self.select_filled_orders('sell'))
|
||||
|
||||
@property
|
||||
def sell_reason(self) -> str:
|
||||
""" DEPRECATED! Please use exit_reason instead."""
|
||||
return self.exit_reason
|
||||
|
||||
@staticmethod
|
||||
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
||||
open_date: datetime = None, close_date: datetime = None,
|
||||
@@ -874,12 +1107,25 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
max_rate = Column(Float, nullable=True, default=0.0)
|
||||
# Lowest price reached
|
||||
min_rate = Column(Float, nullable=True)
|
||||
sell_reason = Column(String(100), nullable=True)
|
||||
sell_order_status = Column(String(100), nullable=True)
|
||||
exit_reason = Column(String(100), nullable=True)
|
||||
exit_order_status = Column(String(100), nullable=True)
|
||||
strategy = Column(String(100), nullable=True)
|
||||
buy_tag = Column(String(100), nullable=True)
|
||||
enter_tag = Column(String(100), nullable=True)
|
||||
timeframe = Column(Integer, nullable=True)
|
||||
|
||||
trading_mode = Column(Enum(TradingMode), nullable=True)
|
||||
|
||||
# Leverage trading properties
|
||||
leverage = Column(Float, nullable=True, default=1.0)
|
||||
is_short = Column(Boolean, nullable=False, default=False)
|
||||
liquidation_price = Column(Float, nullable=True)
|
||||
|
||||
# Margin Trading Properties
|
||||
interest_rate = Column(Float, nullable=False, default=0.0)
|
||||
|
||||
# Futures properties
|
||||
funding_fees = Column(Float, nullable=True, default=None)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.realized_profit = 0
|
||||
@@ -967,7 +1213,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
]).all()
|
||||
|
||||
@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
|
||||
NOTE: Not supported in Backtesting.
|
||||
@@ -1036,7 +1282,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_buy_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
def get_enter_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, based on buy tag performance
|
||||
Can either be average for all pairs or a specific pair provided
|
||||
@@ -1047,31 +1293,31 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
if(pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
buy_tag_perf = Trade.query.with_entities(
|
||||
Trade.buy_tag,
|
||||
enter_tag_perf = Trade.query.with_entities(
|
||||
Trade.enter_tag,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.buy_tag) \
|
||||
.group_by(Trade.enter_tag) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
||||
return [
|
||||
{
|
||||
'buy_tag': buy_tag if buy_tag is not None else "Other",
|
||||
'enter_tag': enter_tag if enter_tag is not None else "Other",
|
||||
'profit_ratio': profit,
|
||||
'profit_pct': round(profit * 100, 2),
|
||||
'profit_abs': profit_abs,
|
||||
'count': count
|
||||
}
|
||||
for buy_tag, profit, profit_abs, count in buy_tag_perf
|
||||
for enter_tag, profit, profit_abs, count in enter_tag_perf
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_sell_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
def get_exit_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, based on sell reason performance
|
||||
Returns List of dicts containing all Trades, based on exit reason performance
|
||||
Can either be average for all pairs or a specific pair provided
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
@@ -1081,30 +1327,30 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
sell_tag_perf = Trade.query.with_entities(
|
||||
Trade.sell_reason,
|
||||
Trade.exit_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.sell_reason) \
|
||||
.group_by(Trade.exit_reason) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
||||
return [
|
||||
{
|
||||
'sell_reason': sell_reason if sell_reason is not None else "Other",
|
||||
'exit_reason': exit_reason if exit_reason is not None else "Other",
|
||||
'profit_ratio': profit,
|
||||
'profit_pct': round(profit * 100, 2),
|
||||
'profit_abs': profit_abs,
|
||||
'count': count
|
||||
}
|
||||
for sell_reason, profit, profit_abs, count in sell_tag_perf
|
||||
for exit_reason, profit, profit_abs, count in sell_tag_perf
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, based on buy_tag + sell_reason performance
|
||||
Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
|
||||
Can either be average for all pairs or a specific pair provided
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
@@ -1115,8 +1361,8 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
|
||||
mix_tag_perf = Trade.query.with_entities(
|
||||
Trade.id,
|
||||
Trade.buy_tag,
|
||||
Trade.sell_reason,
|
||||
Trade.enter_tag,
|
||||
Trade.exit_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
@@ -1126,12 +1372,12 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
.all()
|
||||
|
||||
return_list: List[Dict] = []
|
||||
for id, buy_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
|
||||
buy_tag = buy_tag if buy_tag is not None else "Other"
|
||||
sell_reason = sell_reason if sell_reason is not None else "Other"
|
||||
for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf:
|
||||
enter_tag = enter_tag if enter_tag is not None else "Other"
|
||||
exit_reason = exit_reason if exit_reason is not None else "Other"
|
||||
|
||||
if(sell_reason is not None and buy_tag is not None):
|
||||
mix_tag = buy_tag + " " + sell_reason
|
||||
if(exit_reason is not None and enter_tag is not None):
|
||||
mix_tag = enter_tag + " " + exit_reason
|
||||
i = 0
|
||||
if not any(item["mix_tag"] == mix_tag for item in return_list):
|
||||
return_list.append({'mix_tag': mix_tag,
|
||||
|
||||
1499
freqtrade/persistence/models.py.orig
Normal file
1499
freqtrade/persistence/models.py.orig
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user