Merge branch 'develop' into feat/freqai

This commit is contained in:
Matthias
2022-08-09 06:22:57 +02:00
52 changed files with 1976 additions and 634 deletions

View File

@@ -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']

View File

@@ -319,6 +319,10 @@ CONF_SCHEMA = {
'type': 'string',
'enum': ['off', 'ohlc'],
},
'strategy_msg': {
'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS,
},
}
},
'reload': {'type': 'boolean'},

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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)

View File

@@ -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')

View File

@@ -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

View File

@@ -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(

View File

@@ -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()

View File

@@ -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):

View File

@@ -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:

View File

@@ -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')

View File

@@ -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():

View 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:

View File

@@ -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()

View File

@@ -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({

View File

@@ -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'

View File

@@ -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)

View File

@@ -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 }}",

View File

@@ -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