Merge branch 'develop' into feat/freqai
This commit is contained in:
@@ -67,7 +67,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"type": "text",
|
||||
"name": "stake_amount",
|
||||
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||
"default": "100",
|
||||
"default": "unlimited",
|
||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
||||
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
||||
if val == UNLIMITED_STAKE_AMOUNT
|
||||
@@ -164,7 +164,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"when": lambda x: x['telegram']
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"type": "password",
|
||||
"name": "telegram_chat_id",
|
||||
"message": "Insert Telegram chat id",
|
||||
"when": lambda x: x['telegram']
|
||||
@@ -191,7 +191,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"when": lambda x: x['api_server']
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"type": "password",
|
||||
"name": "api_server_password",
|
||||
"message": "Insert api-server password",
|
||||
"when": lambda x: x['api_server']
|
||||
|
@@ -319,6 +319,10 @@ CONF_SCHEMA = {
|
||||
'type': 'string',
|
||||
'enum': ['off', 'ohlc'],
|
||||
},
|
||||
'strategy_msg': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
},
|
||||
}
|
||||
},
|
||||
'reload': {'type': 'boolean'},
|
||||
|
@@ -5,12 +5,14 @@ including ticker and orderbook data, live and historical candle (OHLCV) data
|
||||
Common Interface for bot and strategy to access data.
|
||||
"""
|
||||
import logging
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.configuration.PeriodicCache import PeriodicCache
|
||||
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||
from freqtrade.data.history import load_pair_history
|
||||
from freqtrade.enums import CandleType, RunMode
|
||||
@@ -33,6 +35,10 @@ class DataProvider:
|
||||
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
||||
self.__slice_index: Optional[int] = None
|
||||
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
||||
self._msg_queue: deque = deque()
|
||||
|
||||
self.__msg_cache = PeriodicCache(
|
||||
maxsize=1000, ttl=timeframe_to_seconds(self._config.get('timeframe', '1h')))
|
||||
|
||||
def _set_dataframe_max_index(self, limit_index: int):
|
||||
"""
|
||||
@@ -265,3 +271,20 @@ class DataProvider:
|
||||
if self._exchange is None:
|
||||
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
||||
return self._exchange.fetch_l2_order_book(pair, maximum)
|
||||
|
||||
def send_msg(self, message: str, *, always_send: bool = False) -> None:
|
||||
"""
|
||||
Send custom RPC Notifications from your bot.
|
||||
Will not send any bot in modes other than Dry-run or Live.
|
||||
:param message: Message to be sent. Must be below 4096.
|
||||
:param always_send: If False, will send the message only once per candle, and surpress
|
||||
identical messages.
|
||||
Careful as this can end up spaming your chat.
|
||||
Defaults to False
|
||||
"""
|
||||
if self.runmode not in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||
return
|
||||
|
||||
if always_send or message not in self.__msg_cache:
|
||||
self._msg_queue.append(message)
|
||||
self.__msg_cache[message] = True
|
||||
|
@@ -9,10 +9,12 @@ class ExitType(Enum):
|
||||
STOP_LOSS = "stop_loss"
|
||||
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||
LIQUIDATION = "liquidation"
|
||||
EXIT_SIGNAL = "exit_signal"
|
||||
FORCE_EXIT = "force_exit"
|
||||
EMERGENCY_EXIT = "emergency_exit"
|
||||
CUSTOM_EXIT = "custom_exit"
|
||||
PARTIAL_EXIT = "partial_exit"
|
||||
NONE = ""
|
||||
|
||||
def __str__(self):
|
||||
|
@@ -17,6 +17,8 @@ class RPCMessageType(Enum):
|
||||
PROTECTION_TRIGGER = 'protection_trigger'
|
||||
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||
|
||||
STRATEGY_MSG = 'strategy_msg'
|
||||
|
||||
def __repr__(self):
|
||||
return self.value
|
||||
|
||||
|
@@ -1332,11 +1332,19 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def fetch_positions(self) -> List[Dict]:
|
||||
def fetch_positions(self, pair: str = None) -> List[Dict]:
|
||||
"""
|
||||
Fetch positions from the exchange.
|
||||
If no pair is given, all positions are returned.
|
||||
:param pair: Pair for the query
|
||||
"""
|
||||
if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES:
|
||||
return []
|
||||
try:
|
||||
positions: List[Dict] = self._api.fetch_positions()
|
||||
symbols = []
|
||||
if pair:
|
||||
symbols.append(pair)
|
||||
positions: List[Dict] = self._api.fetch_positions(symbols)
|
||||
self._log_exchange_response('fetch_positions', positions)
|
||||
return positions
|
||||
except ccxt.DDoSProtection as e:
|
||||
@@ -1499,7 +1507,8 @@ class Exchange:
|
||||
return price_side
|
||||
|
||||
def get_rate(self, pair: str, refresh: bool,
|
||||
side: EntryExit, is_short: bool) -> float:
|
||||
side: EntryExit, is_short: bool,
|
||||
order_book: Optional[dict] = None, ticker: Optional[dict] = None) -> float:
|
||||
"""
|
||||
Calculates bid/ask target
|
||||
bid rate - between current ask price and last price
|
||||
@@ -1531,22 +1540,24 @@ class Exchange:
|
||||
if conf_strategy.get('use_order_book', False):
|
||||
|
||||
order_book_top = conf_strategy.get('order_book_top', 1)
|
||||
order_book = self.fetch_l2_order_book(pair, order_book_top)
|
||||
if order_book is None:
|
||||
order_book = self.fetch_l2_order_book(pair, order_book_top)
|
||||
logger.debug('order_book %s', order_book)
|
||||
# top 1 = index 0
|
||||
try:
|
||||
rate = order_book[f"{price_side}s"][order_book_top - 1][0]
|
||||
except (IndexError, KeyError) as e:
|
||||
logger.warning(
|
||||
f"{name} Price at location {order_book_top} from orderbook could not be "
|
||||
f"determined. Orderbook: {order_book}"
|
||||
f"{pair} - {name} Price at location {order_book_top} from orderbook "
|
||||
f"could not be determined. Orderbook: {order_book}"
|
||||
)
|
||||
raise PricingError from e
|
||||
logger.debug(f"{name} price from orderbook {price_side_word}"
|
||||
logger.debug(f"{pair} - {name} price from orderbook {price_side_word}"
|
||||
f"side - top {order_book_top} order book {side} rate {rate:.8f}")
|
||||
else:
|
||||
logger.debug(f"Using Last {price_side_word} / Last Price")
|
||||
ticker = self.fetch_ticker(pair)
|
||||
if ticker is None:
|
||||
ticker = self.fetch_ticker(pair)
|
||||
ticker_rate = ticker[price_side]
|
||||
if ticker['last'] and ticker_rate:
|
||||
if side == 'entry' and ticker_rate > ticker['last']:
|
||||
@@ -1563,6 +1574,33 @@ class Exchange:
|
||||
|
||||
return rate
|
||||
|
||||
def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
|
||||
entry_rate = None
|
||||
exit_rate = None
|
||||
if not refresh:
|
||||
entry_rate = self._entry_rate_cache.get(pair)
|
||||
exit_rate = self._exit_rate_cache.get(pair)
|
||||
if entry_rate:
|
||||
logger.debug(f"Using cached buy rate for {pair}.")
|
||||
if exit_rate:
|
||||
logger.debug(f"Using cached sell rate for {pair}.")
|
||||
|
||||
entry_pricing = self._config.get('entry_pricing', {})
|
||||
exit_pricing = self._config.get('exit_pricing', {})
|
||||
order_book = ticker = None
|
||||
if not entry_rate and entry_pricing.get('use_order_book', False):
|
||||
order_book_top = max(entry_pricing.get('order_book_top', 1),
|
||||
exit_pricing.get('order_book_top', 1))
|
||||
order_book = self.fetch_l2_order_book(pair, order_book_top)
|
||||
entry_rate = self.get_rate(pair, refresh, 'entry', is_short, order_book=order_book)
|
||||
elif not entry_rate:
|
||||
ticker = self.fetch_ticker(pair)
|
||||
entry_rate = self.get_rate(pair, refresh, 'entry', is_short, ticker=ticker)
|
||||
if not exit_rate:
|
||||
exit_rate = self.get_rate(pair, refresh, 'exit',
|
||||
is_short, order_book=order_book, ticker=ticker)
|
||||
return entry_rate, exit_rate
|
||||
|
||||
# Fee handling
|
||||
|
||||
@retrier
|
||||
@@ -1981,7 +2019,7 @@ class Exchange:
|
||||
else:
|
||||
logger.debug(
|
||||
"Fetching trades for pair %s, since %s %s...",
|
||||
pair, since,
|
||||
pair, since,
|
||||
'(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
|
||||
)
|
||||
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
|
||||
@@ -2539,7 +2577,6 @@ class Exchange:
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
@retrier
|
||||
def get_or_calculate_liquidation_price(
|
||||
self,
|
||||
pair: str,
|
||||
@@ -2573,20 +2610,12 @@ class Exchange:
|
||||
upnl_ex_1=upnl_ex_1
|
||||
)
|
||||
else:
|
||||
try:
|
||||
positions = self._api.fetch_positions([pair])
|
||||
if len(positions) > 0:
|
||||
pos = positions[0]
|
||||
isolated_liq = pos['liquidationPrice']
|
||||
else:
|
||||
return None
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
positions = self.fetch_positions(pair)
|
||||
if len(positions) > 0:
|
||||
pos = positions[0]
|
||||
isolated_liq = pos['liquidationPrice']
|
||||
else:
|
||||
return None
|
||||
|
||||
if isolated_liq:
|
||||
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
|
||||
|
@@ -5,6 +5,7 @@ import copy
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import datetime, time, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from math import isclose
|
||||
from threading import Lock
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
@@ -25,7 +26,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
|
||||
from freqtrade.persistence import Order, PairLocks, Trade, init_db
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
@@ -149,7 +150,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.check_for_open_trades()
|
||||
|
||||
self.rpc.cleanup()
|
||||
cleanup_db()
|
||||
Trade.commit()
|
||||
self.exchange.close()
|
||||
|
||||
def startup(self) -> None:
|
||||
@@ -214,6 +215,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
self._schedule.run_pending()
|
||||
Trade.commit()
|
||||
self.rpc.process_msg_queue(self.dataprovider._msg_queue)
|
||||
self.last_process = datetime.now(timezone.utc)
|
||||
|
||||
def process_stopped(self) -> None:
|
||||
@@ -524,39 +526,61 @@ class FreqtradeBot(LoggingMixin):
|
||||
If the strategy triggers the adjustment, a new order gets issued.
|
||||
Once that completes, the existing trade is modified to match new data.
|
||||
"""
|
||||
if self.strategy.max_entry_position_adjustment > -1:
|
||||
count_of_buys = trade.nr_of_successful_entries
|
||||
if count_of_buys > self.strategy.max_entry_position_adjustment:
|
||||
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
|
||||
return
|
||||
else:
|
||||
logger.debug("Max adjustment entries is set to unlimited.")
|
||||
current_rate = self.exchange.get_rate(
|
||||
trade.pair, side='entry', is_short=trade.is_short, refresh=True)
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
current_entry_rate, current_exit_rate = self.exchange.get_rates(
|
||||
trade.pair, True, trade.is_short)
|
||||
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair,
|
||||
current_rate,
|
||||
self.strategy.stoploss)
|
||||
max_stake_amount = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
||||
current_entry_profit = trade.calc_profit_ratio(current_entry_rate)
|
||||
current_exit_profit = trade.calc_profit_ratio(current_exit_rate)
|
||||
|
||||
min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
|
||||
current_entry_rate,
|
||||
self.strategy.stoploss)
|
||||
min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
|
||||
current_exit_rate,
|
||||
self.strategy.stoploss)
|
||||
max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate)
|
||||
stake_available = self.wallets.get_available_stake_amount()
|
||||
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||
default_retval=None)(
|
||||
trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_rate,
|
||||
current_profit=current_profit, min_stake=min_stake_amount,
|
||||
max_stake=min(max_stake_amount, stake_available))
|
||||
trade=trade,
|
||||
current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
|
||||
current_profit=current_entry_profit, min_stake=min_entry_stake,
|
||||
max_stake=min(max_entry_stake, stake_available),
|
||||
current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate,
|
||||
current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit
|
||||
)
|
||||
|
||||
if stake_amount is not None and stake_amount > 0.0:
|
||||
# We should increase our position
|
||||
self.execute_entry(trade.pair, stake_amount, price=current_rate,
|
||||
if self.strategy.max_entry_position_adjustment > -1:
|
||||
count_of_entries = trade.nr_of_successful_entries
|
||||
if count_of_entries > self.strategy.max_entry_position_adjustment:
|
||||
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
|
||||
return
|
||||
else:
|
||||
logger.debug("Max adjustment entries is set to unlimited.")
|
||||
self.execute_entry(trade.pair, stake_amount, price=current_entry_rate,
|
||||
trade=trade, is_short=trade.is_short)
|
||||
|
||||
if stake_amount is not None and stake_amount < 0.0:
|
||||
# We should decrease our position
|
||||
# TODO: Selling part of the trade not implemented yet.
|
||||
logger.error(f"Unable to decrease trade position / sell partially"
|
||||
f" for pair {trade.pair}, feature not implemented.")
|
||||
amount = abs(float(Decimal(stake_amount) / Decimal(current_exit_rate)))
|
||||
if amount > trade.amount:
|
||||
# This is currently ineffective as remaining would become < min tradable
|
||||
# Fixing this would require checking for 0.0 there -
|
||||
# if we decide that this callback is allowed to "fully exit"
|
||||
logger.info(
|
||||
f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}")
|
||||
amount = trade.amount
|
||||
|
||||
remaining = (trade.amount - amount) * current_exit_rate
|
||||
if remaining < min_exit_stake:
|
||||
logger.info(f'Remaining amount of {remaining} would be too small.')
|
||||
return
|
||||
|
||||
self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple(
|
||||
exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount)
|
||||
|
||||
def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool:
|
||||
"""
|
||||
@@ -730,7 +754,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Updating wallets
|
||||
self.wallets.update()
|
||||
|
||||
self._notify_enter(trade, order, order_type)
|
||||
self._notify_enter(trade, order_obj, order_type, sub_trade=pos_adjust)
|
||||
|
||||
if pos_adjust:
|
||||
if order_status == 'closed':
|
||||
@@ -739,8 +763,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
else:
|
||||
logger.info(f"DCA order {order_status}, will wait for resolution: {trade}")
|
||||
|
||||
# Update fees if order is closed
|
||||
if order_status == 'closed':
|
||||
# Update fees if order is non-opened
|
||||
if order_status in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
self.update_trade_state(trade, order_id, order)
|
||||
|
||||
return True
|
||||
@@ -829,13 +853,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
return enter_limit_requested, stake_amount, leverage
|
||||
|
||||
def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None,
|
||||
fill: bool = False) -> None:
|
||||
def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None,
|
||||
fill: bool = False, sub_trade: bool = False) -> None:
|
||||
"""
|
||||
Sends rpc notification when a entry order occurred.
|
||||
"""
|
||||
msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
|
||||
open_rate = safe_value_fallback(order, 'average', 'price')
|
||||
open_rate = order.safe_price
|
||||
|
||||
if open_rate is None:
|
||||
open_rate = trade.open_rate
|
||||
|
||||
@@ -859,15 +884,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'amount': safe_value_fallback(order, 'filled', 'amount') or trade.amount,
|
||||
'amount': order.safe_amount_after_fee,
|
||||
'open_date': trade.open_date or datetime.utcnow(),
|
||||
'current_rate': current_rate,
|
||||
'sub_trade': sub_trade,
|
||||
}
|
||||
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||
def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str,
|
||||
sub_trade: bool = False) -> None:
|
||||
"""
|
||||
Sends rpc notification when a entry order cancel occurred.
|
||||
"""
|
||||
@@ -892,6 +919,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'open_date': trade.open_date,
|
||||
'current_rate': current_rate,
|
||||
'reason': reason,
|
||||
'sub_trade': sub_trade,
|
||||
}
|
||||
|
||||
# Send the message
|
||||
@@ -1015,7 +1043,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.stoploss_order_id = None
|
||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||
logger.warning('Exiting the trade forcefully')
|
||||
self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple(
|
||||
self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple(
|
||||
exit_type=ExitType.EMERGENCY_EXIT))
|
||||
|
||||
except ExchangeError:
|
||||
@@ -1085,7 +1113,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if (trade.is_open
|
||||
and stoploss_order
|
||||
and stoploss_order['status'] in ('canceled', 'cancelled')):
|
||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
|
||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
|
||||
return False
|
||||
else:
|
||||
trade.stoploss_order_id = None
|
||||
@@ -1114,7 +1142,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
:param order: Current on exchange stoploss order
|
||||
:return: None
|
||||
"""
|
||||
stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stop_loss)
|
||||
stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation)
|
||||
|
||||
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
|
||||
# we check if the update is necessary
|
||||
@@ -1132,7 +1160,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
f"for pair {trade.pair}")
|
||||
|
||||
# Create new stoploss order
|
||||
if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
|
||||
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
|
||||
logger.warning(f"Could not create trailing stoploss order "
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
@@ -1365,16 +1393,22 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.open_order_id = None
|
||||
trade.exit_reason = None
|
||||
cancelled = True
|
||||
self.wallets.update()
|
||||
else:
|
||||
# TODO: figure out how to handle partially complete sell orders
|
||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
cancelled = False
|
||||
|
||||
self.wallets.update()
|
||||
order_obj = trade.select_order_by_order_id(order['id'])
|
||||
if not order_obj:
|
||||
raise DependencyException(
|
||||
f"Order_obj not found for {order['id']}. This should not have happened.")
|
||||
|
||||
sub_trade = order_obj.amount != trade.amount
|
||||
self._notify_exit_cancel(
|
||||
trade,
|
||||
order_type=self.strategy.order_types['exit'],
|
||||
reason=reason
|
||||
reason=reason, order=order_obj, sub_trade=sub_trade
|
||||
)
|
||||
return cancelled
|
||||
|
||||
@@ -1415,6 +1449,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
*,
|
||||
exit_tag: Optional[str] = None,
|
||||
ordertype: Optional[str] = None,
|
||||
sub_trade_amt: float = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Executes a trade exit for the given trade and limit
|
||||
@@ -1431,14 +1466,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
exit_type = 'exit'
|
||||
exit_reason = exit_tag or exit_check.exit_reason
|
||||
if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
|
||||
if exit_check.exit_type in (
|
||||
ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION):
|
||||
exit_type = 'stoploss'
|
||||
|
||||
# if stoploss is on exchange and we are on dry_run mode,
|
||||
# we consider the sell price stop price
|
||||
if (self.config['dry_run'] and exit_type == 'stoploss'
|
||||
and self.strategy.order_types['stoploss_on_exchange']):
|
||||
limit = trade.stop_loss
|
||||
and self.strategy.order_types['stoploss_on_exchange']):
|
||||
limit = trade.stoploss_or_liquidation
|
||||
|
||||
# set custom_exit_price if available
|
||||
proposed_limit_rate = limit
|
||||
@@ -1460,14 +1496,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Emergency sells (default to market!)
|
||||
order_type = self.strategy.order_types.get("emergency_exit", "market")
|
||||
|
||||
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||
amount = self._safe_exit_amount(trade.pair, sub_trade_amt or trade.amount)
|
||||
time_in_force = self.strategy.order_time_in_force['exit']
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
|
||||
time_in_force=time_in_force, exit_reason=exit_reason,
|
||||
sell_reason=exit_reason, # sellreason -> compatibility
|
||||
current_time=datetime.now(timezone.utc)):
|
||||
if (exit_check.exit_type != ExitType.LIQUIDATION
|
||||
and not sub_trade_amt
|
||||
and not strategy_safe_wrapper(
|
||||
self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
|
||||
time_in_force=time_in_force, exit_reason=exit_reason,
|
||||
sell_reason=exit_reason, # sellreason -> compatibility
|
||||
current_time=datetime.now(timezone.utc))):
|
||||
logger.info(f"User denied exit for {trade.pair}.")
|
||||
return False
|
||||
|
||||
@@ -1501,7 +1540,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
|
||||
self._notify_exit(trade, order_type)
|
||||
self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
@@ -1509,16 +1548,27 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
return True
|
||||
|
||||
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None:
|
||||
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False,
|
||||
sub_trade: bool = False, order: Order = None) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell occurred.
|
||||
"""
|
||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||
# Use cached rates here - it was updated seconds ago.
|
||||
current_rate = self.exchange.get_rate(
|
||||
trade.pair, side='exit', is_short=trade.is_short, refresh=False) if not fill else None
|
||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||
|
||||
# second condition is for mypy only; order will always be passed during sub trade
|
||||
if sub_trade and order is not None:
|
||||
amount = order.safe_filled if fill else order.amount
|
||||
profit_rate = order.safe_price
|
||||
|
||||
profit = trade.calc_profit(rate=profit_rate, amount=amount, open_rate=trade.open_rate)
|
||||
profit_ratio = trade.calc_profit_ratio(profit_rate, amount, trade.open_rate)
|
||||
else:
|
||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
profit = trade.calc_profit(rate=profit_rate) + (0.0 if fill else trade.realized_profit)
|
||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||
amount = trade.amount
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
||||
msg = {
|
||||
@@ -1532,11 +1582,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
'gain': gain,
|
||||
'limit': profit_rate,
|
||||
'order_type': order_type,
|
||||
'amount': trade.amount,
|
||||
'amount': amount,
|
||||
'open_rate': trade.open_rate,
|
||||
'close_rate': trade.close_rate,
|
||||
'close_rate': profit_rate,
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
'profit_amount': profit,
|
||||
'profit_ratio': profit_ratio,
|
||||
'buy_tag': trade.enter_tag,
|
||||
'enter_tag': trade.enter_tag,
|
||||
@@ -1544,19 +1594,18 @@ class FreqtradeBot(LoggingMixin):
|
||||
'exit_reason': trade.exit_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.utcnow(),
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency'),
|
||||
'sub_trade': sub_trade,
|
||||
'cumulative_profit': trade.realized_profit,
|
||||
}
|
||||
|
||||
if 'fiat_display_currency' in self.config:
|
||||
msg.update({
|
||||
'fiat_currency': self.config['fiat_display_currency'],
|
||||
})
|
||||
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
|
||||
order: Order, sub_trade: bool = False) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell cancel occurred.
|
||||
"""
|
||||
@@ -1582,7 +1631,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'gain': gain,
|
||||
'limit': profit_rate or 0,
|
||||
'order_type': order_type,
|
||||
'amount': trade.amount,
|
||||
'amount': order.safe_amount_after_fee,
|
||||
'open_rate': trade.open_rate,
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
@@ -1596,6 +1645,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'reason': reason,
|
||||
'sub_trade': sub_trade,
|
||||
'stake_amount': trade.stake_amount,
|
||||
}
|
||||
|
||||
if 'fiat_display_currency' in self.config:
|
||||
@@ -1650,41 +1701,51 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.handle_order_fee(trade, order_obj, order)
|
||||
|
||||
trade.update_trade(order_obj)
|
||||
# TODO: is the below necessary? it's already done in update_trade for filled buys
|
||||
trade.recalc_trade_from_orders()
|
||||
Trade.commit()
|
||||
|
||||
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
if order.get('status') in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
# If a entry order was closed, force update on stoploss on exchange
|
||||
if order.get('side') == trade.entry_side:
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
if not self.edge:
|
||||
# TODO: should shorting/leverage be supported by Edge,
|
||||
# then this will need to be fixed.
|
||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||
if order.get('side') == trade.entry_side or trade.amount > 0:
|
||||
# Must also run for partial exits
|
||||
# TODO: Margin will need to use interest_rate as well.
|
||||
# interest_rate = self.exchange.get_interest_rate()
|
||||
trade.set_isolated_liq(self.exchange.get_liquidation_price(
|
||||
trade.set_liquidation_price(self.exchange.get_liquidation_price(
|
||||
leverage=trade.leverage,
|
||||
pair=trade.pair,
|
||||
amount=trade.amount,
|
||||
open_rate=trade.open_rate,
|
||||
is_short=trade.is_short
|
||||
))
|
||||
if not self.edge:
|
||||
# TODO: should shorting/leverage be supported by Edge,
|
||||
# then this will need to be fixed.
|
||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||
|
||||
# Updating wallets when order is closed
|
||||
self.wallets.update()
|
||||
Trade.commit()
|
||||
|
||||
if not trade.is_open:
|
||||
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||
self._notify_exit(trade, '', True)
|
||||
self.handle_protections(trade.pair, trade.trade_direction)
|
||||
elif send_msg and not trade.open_order_id and not stoploss_order:
|
||||
# Enter fill
|
||||
self._notify_enter(trade, order, fill=True)
|
||||
self.order_close_notify(trade, order_obj, stoploss_order, send_msg)
|
||||
|
||||
return False
|
||||
|
||||
def order_close_notify(
|
||||
self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
|
||||
"""send "fill" notifications"""
|
||||
|
||||
sub_trade = not isclose(order.safe_amount_after_fee,
|
||||
trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
|
||||
if order.ft_order_side == trade.exit_side:
|
||||
# Exit notification
|
||||
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||
self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order)
|
||||
if not trade.is_open:
|
||||
self.handle_protections(trade.pair, trade.trade_direction)
|
||||
elif send_msg and not trade.open_order_id and not stoploss_order:
|
||||
# Enter fill
|
||||
self._notify_enter(trade, order, fill=True, sub_trade=sub_trade)
|
||||
|
||||
def handle_protections(self, pair: str, side: LongShort) -> None:
|
||||
prot_trig = self.protections.stop_per_pair(pair, side=side)
|
||||
if prot_trig:
|
||||
|
138
freqtrade/optimize/backtesting.py
Executable file → Normal file
138
freqtrade/optimize/backtesting.py
Executable file → Normal file
@@ -296,8 +296,8 @@ class Backtesting:
|
||||
|
||||
if unavailable_pairs:
|
||||
raise OperationalException(
|
||||
f"Pairs {', '.join(unavailable_pairs)} got no leverage tiers available. "
|
||||
"It is therefore impossible to backtest with this pair at the moment.")
|
||||
f"Pairs {', '.join(unavailable_pairs)} got no leverage tiers available. "
|
||||
"It is therefore impossible to backtest with this pair at the moment.")
|
||||
else:
|
||||
self.futures_data = {}
|
||||
|
||||
@@ -390,7 +390,8 @@ class Backtesting:
|
||||
Get close rate for backtesting result
|
||||
"""
|
||||
# Special handling if high or low hit STOP_LOSS or ROI
|
||||
if exit.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
|
||||
if exit.exit_type in (
|
||||
ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION):
|
||||
return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur)
|
||||
elif exit.exit_type == (ExitType.ROI):
|
||||
return self._get_close_rate_for_roi(row, trade, exit, trade_dur)
|
||||
@@ -405,11 +406,16 @@ class Backtesting:
|
||||
is_short = trade.is_short or False
|
||||
leverage = trade.leverage or 1.0
|
||||
side_1 = -1 if is_short else 1
|
||||
if exit.exit_type == ExitType.LIQUIDATION and trade.liquidation_price:
|
||||
stoploss_value = trade.liquidation_price
|
||||
else:
|
||||
stoploss_value = trade.stop_loss
|
||||
|
||||
if is_short:
|
||||
if trade.stop_loss < row[LOW_IDX]:
|
||||
if stoploss_value < row[LOW_IDX]:
|
||||
return row[OPEN_IDX]
|
||||
else:
|
||||
if trade.stop_loss > row[HIGH_IDX]:
|
||||
if stoploss_value > row[HIGH_IDX]:
|
||||
return row[OPEN_IDX]
|
||||
|
||||
# Special case: trailing triggers within same candle as trade opened. Assume most
|
||||
@@ -442,7 +448,7 @@ class Backtesting:
|
||||
return max(row[LOW_IDX], stop_rate)
|
||||
|
||||
# Set close_rate to stoploss
|
||||
return trade.stop_loss
|
||||
return stoploss_value
|
||||
|
||||
def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
|
||||
trade_dur: int) -> float:
|
||||
@@ -506,16 +512,20 @@ class Backtesting:
|
||||
|
||||
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
|
||||
) -> LocalTrade:
|
||||
current_profit = trade.calc_profit_ratio(row[OPEN_IDX])
|
||||
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, row[OPEN_IDX], -0.1)
|
||||
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, row[OPEN_IDX])
|
||||
current_rate = row[OPEN_IDX]
|
||||
current_date = row[DATE_IDX].to_pydatetime()
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
|
||||
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
||||
stake_available = self.wallets.get_available_stake_amount()
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||
default_retval=None)(
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
|
||||
current_time=current_date, current_rate=current_rate,
|
||||
current_profit=current_profit, min_stake=min_stake,
|
||||
max_stake=min(max_stake, stake_available))
|
||||
max_stake=min(max_stake, stake_available),
|
||||
current_entry_rate=current_rate, current_exit_rate=current_rate,
|
||||
current_entry_profit=current_profit, current_exit_profit=current_profit)
|
||||
|
||||
# Check if we should increase our position
|
||||
if stake_amount is not None and stake_amount > 0.0:
|
||||
@@ -526,6 +536,24 @@ class Backtesting:
|
||||
self.wallets.update()
|
||||
return pos_trade
|
||||
|
||||
if stake_amount is not None and stake_amount < 0.0:
|
||||
amount = abs(stake_amount) / current_rate
|
||||
if amount > trade.amount:
|
||||
# This is currently ineffective as remaining would become < min tradable
|
||||
amount = trade.amount
|
||||
remaining = (trade.amount - amount) * current_rate
|
||||
if remaining < min_stake:
|
||||
# Remaining stake is too low to be sold.
|
||||
return trade
|
||||
pos_trade = self._exit_trade(trade, row, current_rate, amount)
|
||||
if pos_trade is not None:
|
||||
order = pos_trade.orders[-1]
|
||||
if self._get_order_filled(order.price, row):
|
||||
order.close_bt_order(current_date, trade)
|
||||
trade.recalc_trade_from_orders()
|
||||
self.wallets.update()
|
||||
return pos_trade
|
||||
|
||||
return trade
|
||||
|
||||
def _get_order_filled(self, rate: float, row: Tuple) -> bool:
|
||||
@@ -576,7 +604,7 @@ class Backtesting:
|
||||
if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
|
||||
# Checks and adds an exit tag, after checking that the length of the
|
||||
# row has the length for an exit tag column
|
||||
if(
|
||||
if (
|
||||
len(row) > EXIT_TAG_IDX
|
||||
and row[EXIT_TAG_IDX] is not None
|
||||
and len(row[EXIT_TAG_IDX]) > 0
|
||||
@@ -601,46 +629,53 @@ class Backtesting:
|
||||
# Confirm trade exit:
|
||||
time_in_force = self.strategy.order_time_in_force['exit']
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
if (exit_.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper(
|
||||
self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
pair=trade.pair,
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
order_type='limit',
|
||||
order_type=order_type,
|
||||
amount=trade.amount,
|
||||
rate=close_rate,
|
||||
time_in_force=time_in_force,
|
||||
sell_reason=exit_reason, # deprecated
|
||||
exit_reason=exit_reason,
|
||||
current_time=exit_candle_time):
|
||||
current_time=exit_candle_time)):
|
||||
return None
|
||||
|
||||
trade.exit_reason = exit_reason
|
||||
|
||||
self.order_id_counter += 1
|
||||
order = Order(
|
||||
id=self.order_id_counter,
|
||||
ft_trade_id=trade.id,
|
||||
order_date=exit_candle_time,
|
||||
order_update_date=exit_candle_time,
|
||||
ft_is_open=True,
|
||||
ft_pair=trade.pair,
|
||||
order_id=str(self.order_id_counter),
|
||||
symbol=trade.pair,
|
||||
ft_order_side=trade.exit_side,
|
||||
side=trade.exit_side,
|
||||
order_type=order_type,
|
||||
status="open",
|
||||
price=close_rate,
|
||||
average=close_rate,
|
||||
amount=trade.amount,
|
||||
filled=0,
|
||||
remaining=trade.amount,
|
||||
cost=trade.amount * close_rate,
|
||||
)
|
||||
trade.orders.append(order)
|
||||
return trade
|
||||
|
||||
return self._exit_trade(trade, row, close_rate, trade.amount)
|
||||
return None
|
||||
|
||||
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,
|
||||
close_rate: float, amount: float = None) -> Optional[LocalTrade]:
|
||||
self.order_id_counter += 1
|
||||
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||
order_type = self.strategy.order_types['exit']
|
||||
amount = amount or trade.amount
|
||||
order = Order(
|
||||
id=self.order_id_counter,
|
||||
ft_trade_id=trade.id,
|
||||
order_date=exit_candle_time,
|
||||
order_update_date=exit_candle_time,
|
||||
ft_is_open=True,
|
||||
ft_pair=trade.pair,
|
||||
order_id=str(self.order_id_counter),
|
||||
symbol=trade.pair,
|
||||
ft_order_side=trade.exit_side,
|
||||
side=trade.exit_side,
|
||||
order_type=order_type,
|
||||
status="open",
|
||||
price=close_rate,
|
||||
average=close_rate,
|
||||
amount=amount,
|
||||
filled=0,
|
||||
remaining=amount,
|
||||
cost=amount * close_rate,
|
||||
)
|
||||
trade.orders.append(order)
|
||||
return trade
|
||||
|
||||
def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
|
||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
|
||||
@@ -816,7 +851,7 @@ class Backtesting:
|
||||
|
||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||
|
||||
trade.set_isolated_liq(self.exchange.get_liquidation_price(
|
||||
trade.set_liquidation_price(self.exchange.get_liquidation_price(
|
||||
pair=pair,
|
||||
open_rate=propose_rate,
|
||||
amount=amount,
|
||||
@@ -867,6 +902,8 @@ class Backtesting:
|
||||
# Ignore trade if entry-order did not fill yet
|
||||
continue
|
||||
exit_row = data[pair][-1]
|
||||
self._exit_trade(trade, exit_row, exit_row[OPEN_IDX], trade.amount)
|
||||
trade.orders[-1].close_bt_order(exit_row[DATE_IDX].to_pydatetime(), trade)
|
||||
|
||||
trade.close_date = exit_row[DATE_IDX].to_pydatetime()
|
||||
trade.exit_reason = ExitType.FORCE_EXIT.value
|
||||
@@ -1008,7 +1045,7 @@ class Backtesting:
|
||||
return None
|
||||
return row
|
||||
|
||||
def backtest(self, processed: Dict,
|
||||
def backtest(self, processed: Dict, # noqa: max-complexity: 13
|
||||
start_date: datetime, end_date: datetime,
|
||||
max_open_trades: int = 0, position_stacking: bool = False,
|
||||
enable_protections: bool = False) -> Dict[str, Any]:
|
||||
@@ -1110,14 +1147,19 @@ class Backtesting:
|
||||
if order and self._get_order_filled(order.price, row):
|
||||
order.close_bt_order(current_time, trade)
|
||||
trade.open_order_id = None
|
||||
trade.close_date = current_time
|
||||
trade.close(order.price, show_msg=False)
|
||||
sub_trade = order.safe_amount_after_fee != trade.amount
|
||||
if sub_trade:
|
||||
order.close_bt_order(current_time, trade)
|
||||
trade.recalc_trade_from_orders()
|
||||
else:
|
||||
trade.close_date = current_time
|
||||
trade.close(order.price, show_msg=False)
|
||||
|
||||
# logger.debug(f"{pair} - Backtesting exit {trade}")
|
||||
open_trade_count -= 1
|
||||
open_trades[pair].remove(trade)
|
||||
LocalTrade.close_bt_trade(trade)
|
||||
trades.append(trade)
|
||||
# logger.debug(f"{pair} - Backtesting exit {trade}")
|
||||
open_trade_count -= 1
|
||||
open_trades[pair].remove(trade)
|
||||
LocalTrade.close_bt_trade(trade)
|
||||
trades.append(trade)
|
||||
self.wallets.update()
|
||||
self.run_protections(
|
||||
enable_protections, pair, current_time, trade.trade_direction)
|
||||
|
@@ -639,7 +639,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
|
||||
:param stake_currency: stake-currency - used to correctly name headers
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
if(tag_type == "enter_tag"):
|
||||
if (tag_type == "enter_tag"):
|
||||
headers = _get_line_header("TAG", stake_currency)
|
||||
else:
|
||||
headers = _get_line_header("TAG", stake_currency, 'Sells')
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.persistence.models import cleanup_db, init_db
|
||||
from freqtrade.persistence.models import init_db
|
||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||
from freqtrade.persistence.trade_model import LocalTrade, Order, Trade
|
||||
|
@@ -95,6 +95,7 @@ def migrate_trades_and_orders_table(
|
||||
exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null'))
|
||||
strategy = get_column_def(cols, 'strategy', 'null')
|
||||
enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null'))
|
||||
realized_profit = get_column_def(cols, 'realized_profit', '0.0')
|
||||
|
||||
trading_mode = get_column_def(cols, 'trading_mode', 'null')
|
||||
|
||||
@@ -155,7 +156,7 @@ def migrate_trades_and_orders_table(
|
||||
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
|
||||
interest_rate, funding_fees, realized_profit
|
||||
)
|
||||
select id, lower(exchange), pair, {base_currency} base_currency,
|
||||
{stake_currency} stake_currency,
|
||||
@@ -181,7 +182,7 @@ def migrate_trades_and_orders_table(
|
||||
{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
|
||||
{funding_fees} funding_fees, {realized_profit} realized_profit
|
||||
from {trade_back_name}
|
||||
"""))
|
||||
|
||||
@@ -297,8 +298,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_orders, 'stop_price'):
|
||||
# if not has_column(cols_trades, 'base_currency'):
|
||||
# if ('orders' not in previous_tables
|
||||
# or not has_column(cols_orders, 'stop_price')):
|
||||
if not has_column(cols_trades, 'realized_profit'):
|
||||
logger.info(f"Running database migration for trades - "
|
||||
f"backup: {table_back_name}, {order_table_bak_name}")
|
||||
migrate_trades_and_orders_table(
|
||||
|
@@ -61,11 +61,3 @@ def init_db(db_url: str) -> None:
|
||||
previous_tables = inspect(engine).get_table_names()
|
||||
_DECL_BASE.metadata.create_all(engine)
|
||||
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
|
||||
|
||||
|
||||
def cleanup_db() -> None:
|
||||
"""
|
||||
Flushes all pending operations to disk.
|
||||
:return: None
|
||||
"""
|
||||
Trade.commit()
|
||||
|
@@ -4,13 +4,15 @@ This module contains the class to persist trades into SQLite
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from math import isclose
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
|
||||
UniqueConstraint, desc, func)
|
||||
from sqlalchemy.orm import Query, lazyload, relationship
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort
|
||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES,
|
||||
BuySell, LongShort)
|
||||
from freqtrade.enums import ExitType, TradingMode
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.leverage import interest
|
||||
@@ -176,10 +178,9 @@ class Order(_DECL_BASE):
|
||||
self.remaining = 0
|
||||
self.status = 'closed'
|
||||
self.ft_is_open = False
|
||||
if (self.ft_order_side == trade.entry_side
|
||||
and len(trade.select_filled_orders(trade.entry_side)) == 1):
|
||||
if (self.ft_order_side == trade.entry_side):
|
||||
trade.open_rate = self.price
|
||||
trade.recalc_open_trade_value()
|
||||
trade.recalc_trade_from_orders()
|
||||
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True)
|
||||
|
||||
@staticmethod
|
||||
@@ -195,7 +196,7 @@ class Order(_DECL_BASE):
|
||||
if filtered_orders:
|
||||
oobj = filtered_orders[0]
|
||||
oobj.update_from_ccxt_object(order)
|
||||
Order.query.session.commit()
|
||||
Trade.commit()
|
||||
else:
|
||||
logger.warning(f"Did not find order for {order}.")
|
||||
|
||||
@@ -237,6 +238,7 @@ class LocalTrade():
|
||||
trades: List['LocalTrade'] = []
|
||||
trades_open: List['LocalTrade'] = []
|
||||
total_profit: float = 0
|
||||
realized_profit: float = 0
|
||||
|
||||
id: int = 0
|
||||
|
||||
@@ -302,6 +304,16 @@ class LocalTrade():
|
||||
# Futures properties
|
||||
funding_fees: Optional[float] = None
|
||||
|
||||
@property
|
||||
def stoploss_or_liquidation(self) -> float:
|
||||
if self.liquidation_price:
|
||||
if self.is_short:
|
||||
return min(self.stop_loss, self.liquidation_price)
|
||||
else:
|
||||
return max(self.stop_loss, self.liquidation_price)
|
||||
|
||||
return self.stop_loss
|
||||
|
||||
@property
|
||||
def buy_tag(self) -> Optional[str]:
|
||||
"""
|
||||
@@ -437,6 +449,7 @@ class LocalTrade():
|
||||
if self.close_date else None),
|
||||
'close_timestamp': int(self.close_date.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
|
||||
'realized_profit': self.realized_profit or 0.0,
|
||||
'close_rate': self.close_rate,
|
||||
'close_rate_requested': self.close_rate_requested,
|
||||
'close_profit': self.close_profit, # Deprecated
|
||||
@@ -497,7 +510,7 @@ 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_isolated_liq(self, liquidation_price: Optional[float]):
|
||||
def set_liquidation_price(self, liquidation_price: Optional[float]):
|
||||
"""
|
||||
Method you should use to set self.liquidation price.
|
||||
Assures stop_loss is not passed the liquidation price
|
||||
@@ -506,22 +519,13 @@ class LocalTrade():
|
||||
return
|
||||
self.liquidation_price = liquidation_price
|
||||
|
||||
def _set_stop_loss(self, stop_loss: float, percent: float):
|
||||
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
|
||||
Method used internally to set self.stop_loss.
|
||||
"""
|
||||
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.initial_stop_loss = stop_loss
|
||||
self.stop_loss = stop_loss
|
||||
|
||||
self.stop_loss_pct = -1 * abs(percent)
|
||||
self.stoploss_last_update = datetime.utcnow()
|
||||
@@ -543,18 +547,12 @@ class LocalTrade():
|
||||
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 self.initial_stop_loss_pct is None or refresh:
|
||||
self._set_stop_loss(new_loss, stoploss)
|
||||
self.__set_stop_loss(new_loss, stoploss)
|
||||
self.initial_stop_loss = new_loss
|
||||
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
||||
|
||||
@@ -569,7 +567,7 @@ class LocalTrade():
|
||||
# ? 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_stop_loss(new_loss, stoploss)
|
||||
self.__set_stop_loss(new_loss, stoploss)
|
||||
else:
|
||||
logger.debug(f"{self.pair} - Keeping current stoploss...")
|
||||
|
||||
@@ -601,14 +599,28 @@ class LocalTrade():
|
||||
if self.is_open:
|
||||
payment = "SELL" if self.is_short else "BUY"
|
||||
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
|
||||
self.open_order_id = None
|
||||
# condition to avoid reset value when updating fees
|
||||
if self.open_order_id == order.order_id:
|
||||
self.open_order_id = None
|
||||
else:
|
||||
logger.warning(
|
||||
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
||||
self.recalc_trade_from_orders()
|
||||
elif order.ft_order_side == self.exit_side:
|
||||
if self.is_open:
|
||||
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}.')
|
||||
self.close(order.safe_price)
|
||||
# condition to avoid reset value when updating fees
|
||||
if self.open_order_id == order.order_id:
|
||||
self.open_order_id = None
|
||||
else:
|
||||
logger.warning(
|
||||
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
||||
if isclose(order.safe_amount_after_fee, self.amount, abs_tol=MATH_CLOSE_PREC):
|
||||
self.close(order.safe_price)
|
||||
else:
|
||||
self.recalc_trade_from_orders()
|
||||
elif order.ft_order_side == 'stoploss':
|
||||
self.stoploss_order_id = None
|
||||
self.close_rate_requested = self.stop_loss
|
||||
@@ -627,11 +639,11 @@ class LocalTrade():
|
||||
"""
|
||||
self.close_rate = rate
|
||||
self.close_date = self.close_date or datetime.utcnow()
|
||||
self.close_profit = self.calc_profit_ratio(rate)
|
||||
self.close_profit_abs = self.calc_profit(rate)
|
||||
self.close_profit_abs = self.calc_profit(rate) + self.realized_profit
|
||||
self.is_open = False
|
||||
self.exit_order_status = 'closed'
|
||||
self.open_order_id = None
|
||||
self.recalc_trade_from_orders(is_closing=True)
|
||||
if show_msg:
|
||||
logger.info(
|
||||
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
||||
@@ -677,12 +689,12 @@ class LocalTrade():
|
||||
"""
|
||||
return len([o for o in self.orders if o.ft_order_side == self.exit_side])
|
||||
|
||||
def _calc_open_trade_value(self) -> float:
|
||||
def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
|
||||
"""
|
||||
Calculate the open_rate including open_fee.
|
||||
:return: Price in of the open trade incl. Fees
|
||||
"""
|
||||
open_trade = Decimal(self.amount) * Decimal(self.open_rate)
|
||||
open_trade = Decimal(amount) * Decimal(open_rate)
|
||||
fees = open_trade * Decimal(self.fee_open)
|
||||
if self.is_short:
|
||||
return float(open_trade - fees)
|
||||
@@ -694,7 +706,7 @@ class LocalTrade():
|
||||
Recalculate open_trade_value.
|
||||
Must be called whenever open_rate, fee_open is changed.
|
||||
"""
|
||||
self.open_trade_value = self._calc_open_trade_value()
|
||||
self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate)
|
||||
|
||||
def calculate_interest(self) -> Decimal:
|
||||
"""
|
||||
@@ -726,7 +738,7 @@ class LocalTrade():
|
||||
else:
|
||||
return close_trade - fees
|
||||
|
||||
def calc_close_trade_value(self, rate: float) -> float:
|
||||
def calc_close_trade_value(self, rate: float, amount: float = None) -> float:
|
||||
"""
|
||||
Calculate the Trade's close value including fees
|
||||
:param rate: rate to compare with.
|
||||
@@ -735,96 +747,143 @@ class LocalTrade():
|
||||
if rate is None and not self.close_rate:
|
||||
return 0.0
|
||||
|
||||
amount = Decimal(self.amount)
|
||||
amount1 = Decimal(amount or self.amount)
|
||||
trading_mode = self.trading_mode or TradingMode.SPOT
|
||||
|
||||
if trading_mode == TradingMode.SPOT:
|
||||
return float(self._calc_base_close(amount, rate, self.fee_close))
|
||||
return float(self._calc_base_close(amount1, rate, self.fee_close))
|
||||
|
||||
elif (trading_mode == TradingMode.MARGIN):
|
||||
|
||||
total_interest = self.calculate_interest()
|
||||
|
||||
if self.is_short:
|
||||
amount = amount + total_interest
|
||||
return float(self._calc_base_close(amount, rate, self.fee_close))
|
||||
amount1 = amount1 + total_interest
|
||||
return float(self._calc_base_close(amount1, rate, self.fee_close))
|
||||
else:
|
||||
# Currency already owned for longs, no need to purchase
|
||||
return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest)
|
||||
return float(self._calc_base_close(amount1, rate, self.fee_close) - 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, self.fee_close)) - funding_fees
|
||||
return float(self._calc_base_close(amount1, rate, self.fee_close)) - funding_fees
|
||||
else:
|
||||
return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees
|
||||
return float(self._calc_base_close(amount1, rate, self.fee_close)) + funding_fees
|
||||
else:
|
||||
raise OperationalException(
|
||||
f"{self.trading_mode.value} trading is not yet available using freqtrade")
|
||||
|
||||
def calc_profit(self, rate: float) -> float:
|
||||
def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float:
|
||||
"""
|
||||
Calculate the absolute profit in stake currency between Close and Open trade
|
||||
:param rate: close rate to compare with.
|
||||
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
|
||||
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
|
||||
:return: profit in stake currency as float
|
||||
"""
|
||||
close_trade_value = self.calc_close_trade_value(rate)
|
||||
close_trade_value = self.calc_close_trade_value(rate, amount)
|
||||
if amount is None or open_rate is None:
|
||||
open_trade_value = self.open_trade_value
|
||||
else:
|
||||
open_trade_value = self._calc_open_trade_value(amount, open_rate)
|
||||
|
||||
if self.is_short:
|
||||
profit = self.open_trade_value - close_trade_value
|
||||
profit = open_trade_value - close_trade_value
|
||||
else:
|
||||
profit = close_trade_value - self.open_trade_value
|
||||
profit = close_trade_value - open_trade_value
|
||||
return float(f"{profit:.8f}")
|
||||
|
||||
def calc_profit_ratio(self, rate: float) -> float:
|
||||
def calc_profit_ratio(
|
||||
self, rate: float, amount: float = None, open_rate: float = None) -> float:
|
||||
"""
|
||||
Calculates the profit as ratio (including fee).
|
||||
:param rate: rate to compare with.
|
||||
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
|
||||
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
|
||||
:return: profit ratio as float
|
||||
"""
|
||||
close_trade_value = self.calc_close_trade_value(rate)
|
||||
close_trade_value = self.calc_close_trade_value(rate, amount)
|
||||
|
||||
if amount is None or open_rate is None:
|
||||
open_trade_value = self.open_trade_value
|
||||
else:
|
||||
open_trade_value = self._calc_open_trade_value(amount, open_rate)
|
||||
|
||||
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)
|
||||
long_close_zero = (not self.is_short and open_trade_value == 0.0)
|
||||
leverage = self.leverage or 1.0
|
||||
|
||||
if (short_close_zero or long_close_zero):
|
||||
return 0.0
|
||||
else:
|
||||
if self.is_short:
|
||||
profit_ratio = (1 - (close_trade_value / self.open_trade_value)) * leverage
|
||||
profit_ratio = (1 - (close_trade_value / open_trade_value)) * leverage
|
||||
else:
|
||||
profit_ratio = ((close_trade_value / self.open_trade_value) - 1) * leverage
|
||||
profit_ratio = ((close_trade_value / open_trade_value) - 1) * leverage
|
||||
|
||||
return float(f"{profit_ratio:.8f}")
|
||||
|
||||
def recalc_trade_from_orders(self):
|
||||
def recalc_trade_from_orders(self, is_closing: bool = False):
|
||||
|
||||
current_amount = 0.0
|
||||
current_stake = 0.0
|
||||
total_stake = 0.0 # Total stake after all buy orders (does not subtract!)
|
||||
avg_price = 0.0
|
||||
close_profit = 0.0
|
||||
close_profit_abs = 0.0
|
||||
|
||||
total_amount = 0.0
|
||||
total_stake = 0.0
|
||||
for o in self.orders:
|
||||
if (o.ft_is_open or
|
||||
(o.ft_order_side != self.entry_side) or
|
||||
(o.status not in NON_OPEN_EXCHANGE_STATES)):
|
||||
if o.ft_is_open or not o.filled:
|
||||
continue
|
||||
|
||||
tmp_amount = o.safe_amount_after_fee
|
||||
tmp_price = o.average or o.price
|
||||
if tmp_amount > 0.0 and tmp_price is not None:
|
||||
total_amount += tmp_amount
|
||||
total_stake += tmp_price * tmp_amount
|
||||
tmp_price = o.safe_price
|
||||
|
||||
if total_amount > 0:
|
||||
is_exit = o.ft_order_side != self.entry_side
|
||||
side = -1 if is_exit else 1
|
||||
if tmp_amount > 0.0 and tmp_price is not None:
|
||||
current_amount += tmp_amount * side
|
||||
price = avg_price if is_exit else tmp_price
|
||||
current_stake += price * tmp_amount * side
|
||||
|
||||
if current_amount > 0:
|
||||
avg_price = current_stake / current_amount
|
||||
|
||||
if is_exit:
|
||||
# Process partial exits
|
||||
exit_rate = o.safe_price
|
||||
exit_amount = o.safe_amount_after_fee
|
||||
profit = self.calc_profit(rate=exit_rate, amount=exit_amount, open_rate=avg_price)
|
||||
close_profit_abs += profit
|
||||
close_profit = self.calc_profit_ratio(
|
||||
exit_rate, amount=exit_amount, open_rate=avg_price)
|
||||
if current_amount <= 0:
|
||||
profit = close_profit_abs
|
||||
else:
|
||||
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
|
||||
|
||||
if close_profit:
|
||||
self.close_profit = close_profit
|
||||
self.realized_profit = close_profit_abs
|
||||
self.close_profit_abs = profit
|
||||
|
||||
if current_amount > 0:
|
||||
# Trade is still open
|
||||
# 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.leverage or 1.0)
|
||||
self.amount = total_amount
|
||||
self.fee_open_cost = self.fee_open * total_stake
|
||||
self.open_rate = current_stake / current_amount
|
||||
self.stake_amount = current_stake / (self.leverage or 1.0)
|
||||
self.amount = current_amount
|
||||
self.fee_open_cost = self.fee_open * current_stake
|
||||
self.recalc_open_trade_value()
|
||||
if self.stop_loss_pct is not None and self.open_rate is not None:
|
||||
self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
|
||||
elif is_closing and total_stake > 0:
|
||||
# Close profit abs / maximum owned
|
||||
# Fees are considered as they are part of close_profit_abs
|
||||
self.close_profit = (close_profit_abs / total_stake) * self.leverage
|
||||
|
||||
def select_order_by_order_id(self, order_id: str) -> Optional[Order]:
|
||||
"""
|
||||
@@ -846,7 +905,7 @@ class LocalTrade():
|
||||
"""
|
||||
orders = self.orders
|
||||
if order_side:
|
||||
orders = [o for o in self.orders if o.ft_order_side == order_side]
|
||||
orders = [o for o in orders if o.ft_order_side == order_side]
|
||||
if is_open is not None:
|
||||
orders = [o for o in orders if o.ft_is_open == is_open]
|
||||
if len(orders) > 0:
|
||||
@@ -861,9 +920,9 @@ class LocalTrade():
|
||||
:return: array of Order objects
|
||||
"""
|
||||
return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None))
|
||||
and o.ft_is_open is False and
|
||||
(o.filled or 0) > 0 and
|
||||
o.status in NON_OPEN_EXCHANGE_STATES]
|
||||
and o.ft_is_open is False
|
||||
and o.filled
|
||||
and o.status in NON_OPEN_EXCHANGE_STATES]
|
||||
|
||||
def select_filled_or_open_orders(self) -> List['Order']:
|
||||
"""
|
||||
@@ -1028,6 +1087,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
open_trade_value = Column(Float)
|
||||
close_rate: Optional[float] = Column(Float)
|
||||
close_rate_requested = Column(Float)
|
||||
realized_profit = Column(Float, default=0.0)
|
||||
close_profit = Column(Float)
|
||||
close_profit_abs = Column(Float)
|
||||
stake_amount = Column(Float, nullable=False)
|
||||
@@ -1073,6 +1133,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.realized_profit = 0
|
||||
self.recalc_open_trade_value()
|
||||
|
||||
def delete(self) -> None:
|
||||
@@ -1087,6 +1148,10 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
def commit():
|
||||
Trade.query.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def rollback():
|
||||
Trade.query.session.rollback()
|
||||
|
||||
@staticmethod
|
||||
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
||||
open_date: datetime = None, close_date: datetime = None,
|
||||
@@ -1239,7 +1304,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
"""
|
||||
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if(pair is not None):
|
||||
if (pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
enter_tag_perf = Trade.query.with_entities(
|
||||
@@ -1272,7 +1337,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
"""
|
||||
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if(pair is not None):
|
||||
if (pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
sell_tag_perf = Trade.query.with_entities(
|
||||
@@ -1305,7 +1370,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
"""
|
||||
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if(pair is not None):
|
||||
if (pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
mix_tag_perf = Trade.query.with_entities(
|
||||
@@ -1325,7 +1390,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
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(exit_reason is not None and enter_tag is not None):
|
||||
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):
|
||||
|
@@ -49,7 +49,7 @@ class StoplossGuard(IProtection):
|
||||
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
|
||||
trades = [trade for trade in trades1 if (str(trade.exit_reason) in (
|
||||
ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value,
|
||||
ExitType.STOPLOSS_ON_EXCHANGE.value)
|
||||
ExitType.STOPLOSS_ON_EXCHANGE.value, ExitType.LIQUIDATION.value)
|
||||
and trade.close_profit and trade.close_profit < self._profit_limit)]
|
||||
|
||||
if self._only_per_side:
|
||||
|
@@ -18,9 +18,9 @@ def get_rpc_optional() -> Optional[RPC]:
|
||||
def get_rpc() -> Optional[Iterator[RPC]]:
|
||||
_rpc = get_rpc_optional()
|
||||
if _rpc:
|
||||
Trade.query.session.rollback()
|
||||
Trade.rollback()
|
||||
yield _rpc
|
||||
Trade.query.session.rollback()
|
||||
Trade.rollback()
|
||||
else:
|
||||
raise RPCException('Bot is not in the correct state')
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.exceptions import HTTPException
|
||||
@@ -50,8 +51,12 @@ async def index_html(rest_of_path: str):
|
||||
filename = uibase / rest_of_path
|
||||
# It's security relevant to check "relative_to".
|
||||
# Without this, Directory-traversal is possible.
|
||||
media_type: Optional[str] = None
|
||||
if filename.suffix == '.js':
|
||||
# Force text/javascript for .js files - Circumvent faulty system configuration
|
||||
media_type = 'application/javascript'
|
||||
if filename.is_file() and is_relative_to(filename, uibase):
|
||||
return FileResponse(str(filename))
|
||||
return FileResponse(str(filename), media_type=media_type)
|
||||
|
||||
index_file = uibase / 'index.html'
|
||||
if not index_file.is_file():
|
||||
|
@@ -12,6 +12,7 @@ from pycoingecko import CoinGeckoAPI
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from freqtrade.constants import SUPPORTED_FIAT
|
||||
from freqtrade.mixins.logging_mixin import LoggingMixin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -27,7 +28,7 @@ coingecko_mapping = {
|
||||
}
|
||||
|
||||
|
||||
class CryptoToFiatConverter:
|
||||
class CryptoToFiatConverter(LoggingMixin):
|
||||
"""
|
||||
Main class to initiate Crypto to FIAT.
|
||||
This object contains a list of pair Crypto, FIAT
|
||||
@@ -54,6 +55,7 @@ class CryptoToFiatConverter:
|
||||
# Timeout: 6h
|
||||
self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60)
|
||||
|
||||
LoggingMixin.__init__(self, logger, 3600)
|
||||
self._load_cryptomap()
|
||||
|
||||
def _load_cryptomap(self) -> None:
|
||||
@@ -177,7 +179,9 @@ class CryptoToFiatConverter:
|
||||
|
||||
if not _gekko_id:
|
||||
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
|
||||
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
|
||||
self.log_once(
|
||||
f"unsupported crypto-symbol {crypto_symbol.upper()} - returning 0.0",
|
||||
logger.warning)
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
|
@@ -201,7 +201,7 @@ class RPC:
|
||||
|
||||
trade_dict = trade.to_json()
|
||||
trade_dict.update(dict(
|
||||
close_profit=trade.close_profit if trade.close_profit is not None else None,
|
||||
close_profit=trade.close_profit if not trade.is_open else None,
|
||||
current_rate=current_rate,
|
||||
current_profit=current_profit, # Deprecated
|
||||
current_profit_pct=round(current_profit * 100, 2), # Deprecated
|
||||
@@ -431,14 +431,15 @@ class RPC:
|
||||
|
||||
if not trade.is_open:
|
||||
profit_ratio = trade.close_profit
|
||||
profit_closed_coin.append(trade.close_profit_abs)
|
||||
profit_abs = trade.close_profit_abs
|
||||
profit_closed_coin.append(profit_abs)
|
||||
profit_closed_ratio.append(profit_ratio)
|
||||
if trade.close_profit >= 0:
|
||||
winning_trades += 1
|
||||
winning_profit += trade.close_profit_abs
|
||||
winning_profit += profit_abs
|
||||
else:
|
||||
losing_trades += 1
|
||||
losing_profit += trade.close_profit_abs
|
||||
losing_profit += profit_abs
|
||||
else:
|
||||
# Get current rate
|
||||
try:
|
||||
@@ -447,10 +448,10 @@ class RPC:
|
||||
except (PricingError, ExchangeError):
|
||||
current_rate = NAN
|
||||
profit_ratio = trade.calc_profit_ratio(rate=current_rate)
|
||||
profit_abs = trade.calc_profit(
|
||||
rate=trade.close_rate or current_rate) + trade.realized_profit
|
||||
|
||||
profit_all_coin.append(
|
||||
trade.calc_profit(rate=trade.close_rate or current_rate)
|
||||
)
|
||||
profit_all_coin.append(profit_abs)
|
||||
profit_all_ratio.append(profit_ratio)
|
||||
|
||||
best_pair = Trade.get_best_pair(start_date)
|
||||
@@ -875,7 +876,7 @@ class RPC:
|
||||
lock.active = False
|
||||
lock.lock_end_time = datetime.now(timezone.utc)
|
||||
|
||||
PairLock.query.session.commit()
|
||||
Trade.commit()
|
||||
|
||||
return self._rpc_locks()
|
||||
|
||||
|
@@ -2,6 +2,7 @@
|
||||
This module contains class to manage RPC communications (Telegram, API, ...)
|
||||
"""
|
||||
import logging
|
||||
from collections import deque
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.enums import RPCMessageType
|
||||
@@ -77,6 +78,17 @@ class RPCManager:
|
||||
except NotImplementedError:
|
||||
logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.")
|
||||
|
||||
def process_msg_queue(self, queue: deque) -> None:
|
||||
"""
|
||||
Process all messages in the queue.
|
||||
"""
|
||||
while queue:
|
||||
msg = queue.popleft()
|
||||
self.send_msg({
|
||||
'type': RPCMessageType.STRATEGY_MSG,
|
||||
'msg': msg,
|
||||
})
|
||||
|
||||
def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None:
|
||||
if config['dry_run']:
|
||||
self.send_msg({
|
||||
|
@@ -16,8 +16,8 @@ from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
import arrow
|
||||
from tabulate import tabulate
|
||||
from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton,
|
||||
ParseMode, ReplyKeyboardMarkup, Update)
|
||||
from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup,
|
||||
KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update)
|
||||
from telegram.error import BadRequest, NetworkError, TelegramError
|
||||
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
|
||||
from telegram.utils.helpers import escape_markdown
|
||||
@@ -35,8 +35,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
logger.debug('Included module rpc.telegram ...')
|
||||
|
||||
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
||||
|
||||
|
||||
@dataclass
|
||||
class TimeunitMappings:
|
||||
@@ -72,7 +70,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
||||
)
|
||||
return wrapper
|
||||
# Rollback session to avoid getting data stored in a transaction.
|
||||
Trade.query.session.rollback()
|
||||
Trade.rollback()
|
||||
logger.debug(
|
||||
'Executing handler: %s for chat_id: %s',
|
||||
command_handler.__name__,
|
||||
@@ -274,7 +272,7 @@ class Telegram(RPCHandler):
|
||||
f"{emoji} *{self._exchange_from_msg(msg)}:*"
|
||||
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
|
||||
f" (#{msg['trade_id']})\n"
|
||||
)
|
||||
)
|
||||
message += self._add_analyzed_candle(msg['pair'])
|
||||
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
|
||||
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||
@@ -315,20 +313,36 @@ class Telegram(RPCHandler):
|
||||
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
msg['profit_extra'] = (
|
||||
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
|
||||
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
|
||||
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}")
|
||||
else:
|
||||
msg['profit_extra'] = ''
|
||||
msg['profit_extra'] = (
|
||||
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
|
||||
f"{msg['profit_extra']})")
|
||||
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
|
||||
is_sub_trade = msg.get('sub_trade')
|
||||
is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit')
|
||||
profit_prefix = ('Sub ' if is_sub_profit
|
||||
else 'Cumulative ') if is_sub_trade else ''
|
||||
cp_extra = ''
|
||||
if is_sub_profit and is_sub_trade:
|
||||
if self._rpc._fiat_converter:
|
||||
cp_fiat = self._rpc._fiat_converter.convert_amount(
|
||||
msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency'])
|
||||
cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}"
|
||||
else:
|
||||
cp_extra = ''
|
||||
cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \
|
||||
f"{msg['stake_currency']}{cp_extra}`)\n"
|
||||
message = (
|
||||
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
|
||||
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
|
||||
f"{self._add_analyzed_candle(msg['pair'])}"
|
||||
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
||||
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
|
||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||
f"{cp_extra}"
|
||||
f"*Enter Tag:* `{msg['enter_tag']}`\n"
|
||||
f"*Exit Reason:* `{msg['exit_reason']}`\n"
|
||||
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
|
||||
f"*Direction:* `{msg['direction']}`\n"
|
||||
f"{msg['leverage_text']}"
|
||||
f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||
@@ -336,11 +350,25 @@ class Telegram(RPCHandler):
|
||||
)
|
||||
if msg['type'] == RPCMessageType.EXIT:
|
||||
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||
f"*Close Rate:* `{msg['limit']:.8f}`")
|
||||
f"*Exit Rate:* `{msg['limit']:.8f}`")
|
||||
|
||||
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
||||
message += f"*Close Rate:* `{msg['close_rate']:.8f}`"
|
||||
message += f"*Exit Rate:* `{msg['close_rate']:.8f}`"
|
||||
if msg.get('sub_trade'):
|
||||
if self._rpc._fiat_converter:
|
||||
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
else:
|
||||
msg['stake_amount_fiat'] = 0
|
||||
rem = round_coin_value(msg['stake_amount'], msg['stake_currency'])
|
||||
message += f"\n*Remaining:* `({rem}"
|
||||
|
||||
if msg.get('fiat_currency', None):
|
||||
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
||||
|
||||
message += ")`"
|
||||
else:
|
||||
message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`"
|
||||
return message
|
||||
|
||||
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
|
||||
@@ -353,7 +381,8 @@ class Telegram(RPCHandler):
|
||||
elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
|
||||
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
|
||||
message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
|
||||
f"Cancelling {msg['message_side']} Order for {msg['pair']} "
|
||||
f"Cancelling {'partial ' if msg.get('sub_trade') else ''}"
|
||||
f"{msg['message_side']} Order for {msg['pair']} "
|
||||
f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
|
||||
|
||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
||||
@@ -376,7 +405,8 @@ class Telegram(RPCHandler):
|
||||
|
||||
elif msg_type == RPCMessageType.STARTUP:
|
||||
message = f"{msg['status']}"
|
||||
|
||||
elif msg_type == RPCMessageType.STRATEGY_MSG:
|
||||
message = f"{msg['msg']}"
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown message type: {msg_type}")
|
||||
return message
|
||||
@@ -423,54 +453,63 @@ class Telegram(RPCHandler):
|
||||
else:
|
||||
return "\N{CROSS MARK}"
|
||||
|
||||
def _prepare_entry_details(self, filled_orders: List, quote_currency: str, is_open: bool):
|
||||
def _prepare_order_details(self, filled_orders: List, quote_currency: str, is_open: bool):
|
||||
"""
|
||||
Prepare details of trade with entry adjustment enabled
|
||||
"""
|
||||
lines: List[str] = []
|
||||
lines_detail: List[str] = []
|
||||
if len(filled_orders) > 0:
|
||||
first_avg = filled_orders[0]["safe_price"]
|
||||
|
||||
for x, order in enumerate(filled_orders):
|
||||
if not order['ft_is_entry'] or order['is_open'] is True:
|
||||
lines: List[str] = []
|
||||
if order['is_open'] is True:
|
||||
continue
|
||||
wording = 'Entry' if order['ft_is_entry'] else 'Exit'
|
||||
|
||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||
cur_entry_amount = order["amount"]
|
||||
cur_entry_amount = order["filled"] or order["amount"]
|
||||
cur_entry_average = order["safe_price"]
|
||||
lines.append(" ")
|
||||
if x == 0:
|
||||
lines.append(f"*Entry #{x+1}:*")
|
||||
lines.append(f"*{wording} #{x+1}:*")
|
||||
lines.append(
|
||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
||||
lines.append(f"*Average Entry Price:* {cur_entry_average}")
|
||||
f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
||||
lines.append(f"*Average Price:* {cur_entry_average}")
|
||||
else:
|
||||
sumA = 0
|
||||
sumB = 0
|
||||
for y in range(x):
|
||||
sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"])
|
||||
sumB += filled_orders[y]["amount"]
|
||||
amount = filled_orders[y]["filled"] or filled_orders[y]["amount"]
|
||||
sumA += amount * filled_orders[y]["safe_price"]
|
||||
sumB += amount
|
||||
prev_avg_price = sumA / sumB
|
||||
# TODO: This calculation ignores fees.
|
||||
price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
|
||||
minus_on_entry = 0
|
||||
if prev_avg_price:
|
||||
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
|
||||
|
||||
dur_entry = cur_entry_datetime - arrow.get(
|
||||
filled_orders[x - 1]["order_filled_date"])
|
||||
days = dur_entry.days
|
||||
hours, remainder = divmod(dur_entry.seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit")
|
||||
lines.append(f"*{wording} #{x+1}:* at {minus_on_entry:.2%} avg profit")
|
||||
if is_open:
|
||||
lines.append("({})".format(cur_entry_datetime
|
||||
.humanize(granularity=["day", "hour", "minute"])))
|
||||
lines.append(
|
||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
||||
lines.append(f"*Average Entry Price:* {cur_entry_average} "
|
||||
f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
||||
lines.append(f"*Average {wording} Price:* {cur_entry_average} "
|
||||
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
||||
lines.append(f"*Order filled at:* {order['order_filled_date']}")
|
||||
lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)")
|
||||
return lines
|
||||
lines.append(f"*Order filled:* {order['order_filled_date']}")
|
||||
|
||||
# TODO: is this really useful?
|
||||
# dur_entry = cur_entry_datetime - arrow.get(
|
||||
# filled_orders[x - 1]["order_filled_date"])
|
||||
# days = dur_entry.days
|
||||
# hours, remainder = divmod(dur_entry.seconds, 3600)
|
||||
# minutes, seconds = divmod(remainder, 60)
|
||||
# lines.append(
|
||||
# f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})")
|
||||
lines_detail.append("\n".join(lines))
|
||||
return lines_detail
|
||||
|
||||
@authorized_only
|
||||
def _status(self, update: Update, context: CallbackContext) -> None:
|
||||
@@ -485,7 +524,14 @@ class Telegram(RPCHandler):
|
||||
if context.args and 'table' in context.args:
|
||||
self._status_table(update, context)
|
||||
return
|
||||
else:
|
||||
self._status_msg(update, context)
|
||||
|
||||
def _status_msg(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
handler for `/status` and `/status <id>`.
|
||||
|
||||
"""
|
||||
try:
|
||||
|
||||
# Check if there's at least one numerical ID provided.
|
||||
@@ -497,14 +543,13 @@ class Telegram(RPCHandler):
|
||||
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
|
||||
position_adjust = self._config.get('position_adjustment_enable', False)
|
||||
max_entries = self._config.get('max_entry_position_adjustment', -1)
|
||||
messages = []
|
||||
for r in results:
|
||||
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
|
||||
r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']])
|
||||
r['exit_reason'] = r.get('exit_reason', "")
|
||||
lines = [
|
||||
"*Trade ID:* `{trade_id}`" +
|
||||
("` (since {open_date_hum})`" if r['is_open'] else ""),
|
||||
(" `(since {open_date_hum})`" if r['is_open'] else ""),
|
||||
"*Current Pair:* {pair}",
|
||||
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
|
||||
"*Leverage:* `{leverage}`" if r.get('leverage') else "",
|
||||
@@ -528,6 +573,8 @@ class Telegram(RPCHandler):
|
||||
])
|
||||
|
||||
if r['is_open']:
|
||||
if r.get('realized_profit'):
|
||||
lines.append("*Realized Profit:* `{realized_profit:.8f}`")
|
||||
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
|
||||
and r['initial_stop_loss_ratio'] is not None):
|
||||
# Adding initial stoploss only if it is different from stoploss
|
||||
@@ -540,24 +587,34 @@ class Telegram(RPCHandler):
|
||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
||||
"`({stoploss_current_dist_ratio:.2%})`")
|
||||
if r['open_order']:
|
||||
if r['exit_order_status']:
|
||||
lines.append("*Open Order:* `{open_order}` - `{exit_order_status}`")
|
||||
else:
|
||||
lines.append("*Open Order:* `{open_order}`")
|
||||
lines.append(
|
||||
"*Open Order:* `{open_order}`"
|
||||
+ "- `{exit_order_status}`" if r['exit_order_status'] else "")
|
||||
|
||||
lines_detail = self._prepare_entry_details(
|
||||
lines_detail = self._prepare_order_details(
|
||||
r['orders'], r['quote_currency'], r['is_open'])
|
||||
lines.extend(lines_detail if lines_detail else "")
|
||||
|
||||
# Filter empty lines using list-comprehension
|
||||
messages.append("\n".join([line for line in lines if line]).format(**r))
|
||||
|
||||
for msg in messages:
|
||||
self._send_msg(msg)
|
||||
self.__send_status_msg(lines, r)
|
||||
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Send status message.
|
||||
"""
|
||||
msg = ''
|
||||
|
||||
for line in lines:
|
||||
if line:
|
||||
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
|
||||
msg += line + '\n'
|
||||
else:
|
||||
self._send_msg(msg.format(**r))
|
||||
msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n'
|
||||
|
||||
self._send_msg(msg.format(**r))
|
||||
|
||||
@authorized_only
|
||||
def _status_table(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
@@ -860,7 +917,7 @@ class Telegram(RPCHandler):
|
||||
total_dust_currencies += 1
|
||||
|
||||
# Handle overflowing message length
|
||||
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
if len(output + curr_output) >= MAX_MESSAGE_LENGTH:
|
||||
self._send_msg(output)
|
||||
output = curr_output
|
||||
else:
|
||||
@@ -1123,7 +1180,7 @@ class Telegram(RPCHandler):
|
||||
f"({trade['profit_ratio']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML)
|
||||
output = stat_line
|
||||
else:
|
||||
@@ -1158,7 +1215,7 @@ class Telegram(RPCHandler):
|
||||
f"({trade['profit_ratio']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML)
|
||||
output = stat_line
|
||||
else:
|
||||
@@ -1193,7 +1250,7 @@ class Telegram(RPCHandler):
|
||||
f"({trade['profit_ratio']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML)
|
||||
output = stat_line
|
||||
else:
|
||||
@@ -1228,7 +1285,7 @@ class Telegram(RPCHandler):
|
||||
f"({trade['profit']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML)
|
||||
output = stat_line
|
||||
else:
|
||||
@@ -1367,7 +1424,7 @@ class Telegram(RPCHandler):
|
||||
escape_markdown(logrec[2], version=2),
|
||||
escape_markdown(logrec[3], version=2),
|
||||
escape_markdown(logrec[4], version=2))
|
||||
if len(msgs + msg) + 10 >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH:
|
||||
# Send message immediately if it would become too long
|
||||
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
|
||||
msgs = msg + '\n'
|
||||
|
@@ -472,10 +472,13 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float,
|
||||
min_stake: Optional[float], max_stake: float,
|
||||
current_entry_rate: float, current_exit_rate: float,
|
||||
current_entry_profit: float, current_exit_profit: float,
|
||||
**kwargs) -> Optional[float]:
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||
This means extra buy orders with additional fees.
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be
|
||||
increased or decreased.
|
||||
This means extra buy or sell orders with additional fees.
|
||||
Only called when `position_adjustment_enable` is set to True.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
@@ -486,10 +489,16 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Current buy rate.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param min_stake: Minimal stake size allowed by exchange.
|
||||
:param max_stake: Balance available for trading.
|
||||
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
|
||||
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
|
||||
:param current_entry_rate: Current rate using entry pricing.
|
||||
:param current_exit_rate: Current rate using exit pricing.
|
||||
:param current_entry_profit: Current profit using entry pricing.
|
||||
:param current_exit_profit: Current profit using exit pricing.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: Stake amount to adjust your trade
|
||||
:return float: Stake amount to adjust your trade,
|
||||
Positive values to increase position, Negative values to decrease position.
|
||||
Return None for no action.
|
||||
"""
|
||||
return None
|
||||
|
||||
@@ -988,7 +997,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# ROI
|
||||
# Trailing stoploss
|
||||
|
||||
if stoplossflag.exit_type == ExitType.STOP_LOSS:
|
||||
if stoplossflag.exit_type in (ExitType.STOP_LOSS, ExitType.LIQUIDATION):
|
||||
|
||||
logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
|
||||
exits.append(stoplossflag)
|
||||
@@ -1060,6 +1069,17 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
|
||||
sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short)
|
||||
sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short)
|
||||
liq_higher_long = (trade.liquidation_price
|
||||
and trade.liquidation_price >= (low or current_rate)
|
||||
and not trade.is_short)
|
||||
liq_lower_short = (trade.liquidation_price
|
||||
and trade.liquidation_price <= (high or current_rate)
|
||||
and trade.is_short)
|
||||
|
||||
if (liq_higher_long or liq_lower_short):
|
||||
logger.debug(f"{trade.pair} - Liquidation price hit. exit_type=ExitType.LIQUIDATION")
|
||||
return ExitCheckTuple(exit_type=ExitType.LIQUIDATION)
|
||||
|
||||
# evaluate if the stoploss was hit if stoploss is not on exchange
|
||||
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
||||
# regular stoploss handling.
|
||||
@@ -1077,13 +1097,6 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
f"stoploss is {trade.stop_loss:.6f}, "
|
||||
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
|
||||
f"trade opened at {trade.open_rate:.6f}")
|
||||
new_stoploss = (
|
||||
trade.stop_loss + trade.initial_stop_loss
|
||||
if trade.is_short else
|
||||
trade.stop_loss - trade.initial_stop_loss
|
||||
)
|
||||
logger.debug(f"{trade.pair} - Trailing stop saved "
|
||||
f"{new_stoploss:.6f}")
|
||||
|
||||
return ExitCheckTuple(exit_type=exit_type)
|
||||
|
||||
|
@@ -12,6 +12,7 @@
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "{{ fiat_display_currency }}",{{ ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }}
|
||||
"dry_run": {{ dry_run | lower }},
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": false,
|
||||
"trading_mode": "{{ trading_mode }}",
|
||||
"margin_mode": "{{ margin_mode }}",
|
||||
|
@@ -247,12 +247,16 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||
"""
|
||||
return False
|
||||
|
||||
def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
|
||||
current_rate: float, current_profit: float, min_stake: Optional[float],
|
||||
max_stake: float, **kwargs) -> 'Optional[float]':
|
||||
def adjust_trade_position(self, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float,
|
||||
min_stake: Optional[float], max_stake: float,
|
||||
current_entry_rate: float, current_exit_rate: float,
|
||||
current_entry_profit: float, current_exit_profit: float,
|
||||
**kwargs) -> Optional[float]:
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||
This means extra buy orders with additional fees.
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be
|
||||
increased or decreased.
|
||||
This means extra buy or sell orders with additional fees.
|
||||
Only called when `position_adjustment_enable` is set to True.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
@@ -263,10 +267,16 @@ def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Current buy rate.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param min_stake: Minimal stake size allowed by exchange.
|
||||
:param max_stake: Balance available for trading.
|
||||
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
|
||||
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
|
||||
:param current_entry_rate: Current rate using entry pricing.
|
||||
:param current_exit_rate: Current rate using exit pricing.
|
||||
:param current_entry_profit: Current profit using entry pricing.
|
||||
:param current_exit_profit: Current profit using exit pricing.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: Stake amount to adjust your trade
|
||||
:return float: Stake amount to adjust your trade,
|
||||
Positive values to increase position, Negative values to decrease position.
|
||||
Return None for no action.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
Reference in New Issue
Block a user