diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 35fd3de4a..913bcbfac 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -55,7 +55,7 @@ Currently, the arguments are: * `results`: DataFrame containing the result The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): - `pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, sell_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs` + `pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, close_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs` * `trade_count`: Amount of trades (identical to `len(results)`) * `min_date`: Start date of the timerange used * `min_date`: End date of the timerange used diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 477396931..28036bf65 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -65,7 +65,7 @@ SET is_open=0, close_rate=, close_profit = close_rate / open_rate - 1, close_profit_abs = (amount * * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))), - sell_reason= + close_reason= WHERE id=; ``` @@ -78,7 +78,7 @@ SET is_open=0, close_rate=0.19638016, close_profit=0.0496, close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))), - sell_reason='force_sell' + close_reason='force_sell' WHERE id=31; ``` diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 3436604a9..b33ee55fd 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -49,7 +49,7 @@ from freqtrade.exchange import timeframe_to_prev_date class AwesomeStrategy(IStrategy): def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, + rate: float, time_in_force: str, close_reason: str, current_time: 'datetime', **kwargs) -> bool: # Obtain pair dataframe. dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) @@ -490,7 +490,7 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: + rate: float, time_in_force: str, close_reason: str, **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or @@ -505,14 +505,14 @@ class AwesomeStrategy(IStrategy): :param amount: Amount in quote currency. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). - :param sell_reason: Sell reason. + :param close_reason: Sell reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the sell-order is placed on the exchange. False aborts the process """ - if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: + if close_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: # Reject force-sells with negative profit # This is just a sample, please adjust to your needs # (this does not necessarily make sense, assuming you know when you're force-selling) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 4c938500c..0af44927e 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -127,7 +127,7 @@ print(stats['strategy_comparison']) trades = load_backtest_data(backtest_dir) # Show value-counts per pair -trades.groupby("pair")["sell_reason"].value_counts() +trades.groupby("pair")["close_reason"].value_counts() ``` ### Load live trading results into a pandas dataframe @@ -142,7 +142,7 @@ from freqtrade.data.btanalysis import load_trades_from_db trades = load_trades_from_db("sqlite:///tradesv3.sqlite") # Display results -trades.groupby("pair")["sell_reason"].value_counts() +trades.groupby("pair")["close_reason"].value_counts() ``` ## Analyze the loaded trades for trade parallelism diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 8ce6edc18..1eff10e9e 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -132,7 +132,7 @@ Possible parameters are: * `profit_ratio` * `stake_currency` * `fiat_currency` -* `sell_reason` +* `close_reason` * `order_type` * `open_date` * `close_date` @@ -154,7 +154,7 @@ Possible parameters are: * `profit_ratio` * `stake_currency` * `fiat_currency` -* `sell_reason` +* `close_reason` * `order_type` * `open_date` * `close_date` @@ -176,7 +176,7 @@ Possible parameters are: * `profit_ratio` * `stake_currency` * `fiat_currency` -* `sell_reason` +* `close_reason` * `order_type` * `open_date` * `close_date` diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index e7af5eab8..441a076c3 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -17,18 +17,18 @@ logger = logging.getLogger(__name__) # Old format - maybe remove? BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index", - "trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"] + "trade_duration", "open_rate", "close_rate", "open_at_end", "close_reason"] # Mid-term format, crated by BacktestResult Named Tuple BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration', - 'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open', + 'open_rate', 'close_rate', 'open_at_end', 'close_reason', 'fee_open', 'fee_close', 'amount', 'profit_abs', 'profit_ratio'] # Newest format BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'open_rate', 'close_rate', 'fee_open', 'fee_close', 'trade_duration', - 'profit_ratio', 'profit_abs', 'sell_reason', + 'profit_ratio', 'profit_abs', 'close_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', ] diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a2e7fcb5d..a621e1bfa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -752,7 +752,7 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Selling the trade forcefully') - self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( + self.execute_sell(trade, trade.stop_loss, close_reason=SellCheckTuple( sell_type=SellType.EMERGENCY_SELL)) except ExchangeError: @@ -783,7 +783,7 @@ class FreqtradeBot(LoggingMixin): # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): - trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value + trade.close_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys @@ -1071,16 +1071,16 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: + def execute_sell(self, trade: Trade, limit: float, close_reason: SellCheckTuple) -> bool: """ Executes a limit sell for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order - :param sell_reason: Reason the sell was triggered + :param close_reason: Reason the sell was triggered :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' - if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if close_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, @@ -1099,10 +1099,10 @@ class FreqtradeBot(LoggingMixin): logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") order_type = self.strategy.order_types[sell_type] - if sell_reason.sell_type == SellType.EMERGENCY_SELL: + if close_reason.sell_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") - if sell_reason.sell_type == SellType.FORCE_SELL: + if close_reason.sell_type == SellType.FORCE_SELL: # Force sells (default to the sell_type defined in the strategy, # but we allow this value to be changed) order_type = self.strategy.order_types.get("forcesell", order_type) @@ -1112,7 +1112,7 @@ class FreqtradeBot(LoggingMixin): 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, sell_reason=sell_reason.sell_reason, + time_in_force=time_in_force, close_reason=close_reason.close_reason, current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of selling {trade.pair}") return False @@ -1134,9 +1134,9 @@ class FreqtradeBot(LoggingMixin): trade.orders.append(order_obj) trade.open_order_id = order['id'] - trade.sell_order_status = '' + trade.close_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = sell_reason.sell_reason + trade.close_reason = close_reason.close_reason # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) @@ -1176,7 +1176,7 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, + 'close_reason': trade.close_reason, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], @@ -1195,10 +1195,10 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a sell cancel occurred. """ - if trade.sell_order_status == reason: + if trade.close_order_status == reason: return else: - trade.sell_order_status = reason + trade.close_order_status = reason profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) @@ -1219,7 +1219,7 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, + 'close_reason': trade.close_reason, 'open_date': trade.open_date, 'close_date': trade.close_date, 'stake_currency': self.config['stake_currency'], diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c72a8b5c5..5282b8588 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -267,7 +267,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_row[DATE_IDX].to_pydatetime() - trade.sell_reason = sell.sell_reason + trade.close_reason = sell.close_reason trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -277,7 +277,7 @@ class Backtesting: pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, - sell_reason=sell.sell_reason, + close_reason=sell.close_reason, current_time=sell_row[DATE_IDX].to_pydatetime()): return None @@ -329,7 +329,7 @@ class Backtesting: sell_row = data[pair][-1] trade.close_date = sell_row[DATE_IDX].to_pydatetime() - trade.sell_reason = SellType.FORCE_SELL.value + trade.close_reason = SellType.FORCE_SELL.value trade.close(sell_row[OPEN_IDX], show_msg=False) LocalTrade.close_bt_trade(trade) # Deepcopy object to have wallets update correctly diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 84e052ac4..6a5f8da5c 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -127,7 +127,7 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b return tabular_data -def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: +def generate_close_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: """ Generate small table outlining Backtest results :param max_open_trades: Max_open_trades parameter @@ -136,8 +136,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List """ tabular_data = [] - for reason, count in results['sell_reason'].value_counts().iteritems(): - result = results.loc[results['sell_reason'] == reason] + for reason, count in results['close_reason'].value_counts().iteritems(): + result = results.loc[results['close_reason'] == reason] profit_mean = result['profit_ratio'].mean() profit_sum = result['profit_ratio'].sum() @@ -145,7 +145,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List tabular_data.append( { - 'sell_reason': reason, + 'close_reason': reason, 'trades': count, 'wins': len(result[result['profit_abs'] > 0]), 'draws': len(result[result['profit_abs'] == 0]), @@ -230,7 +230,7 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: draw_trades = results.loc[results['profit_ratio'] == 0] losing_trades = results.loc[results['profit_ratio'] < 0] zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) & - (results['sell_reason'] == 'trailing_stop_loss')]) + (results['close_reason'] == 'trailing_stop_loss')]) holding_avg = (timedelta(minutes=round(results['trade_duration'].mean())) if not results.empty else timedelta()) @@ -313,7 +313,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, starting_balance=starting_balance, results=results, skip_nan=False) - sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, + close_reason_stats = generate_close_reason_stats(max_open_trades=max_open_trades, results=results) left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, starting_balance=starting_balance, @@ -335,7 +335,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'best_pair': best_pair, 'worst_pair': worst_pair, 'results_per_pair': pair_results, - 'sell_reason_summary': sell_reason_stats, + 'close_reason_summary': close_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), 'total_volume': float(results['stake_amount'].sum()), @@ -477,10 +477,10 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") -def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str: +def text_table_close_reason(close_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str: """ Generate small table outlining Backtest results - :param sell_reason_stats: Sell reason metrics + :param close_reason_stats: Sell reason metrics :param stake_currency: Stakecurrency used :return: pretty printed table with tabulate as string """ @@ -495,12 +495,12 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren ] output = [[ - t['sell_reason'], t['trades'], + t['close_reason'], t['trades'], _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), t['profit_mean_pct'], t['profit_sum_pct'], round_coin_value(t['profit_total_abs'], stake_currency, False), t['profit_total_pct'], - ] for t in sell_reason_stats] + ] for t in close_reason_stats] return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") @@ -633,7 +633,7 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], + table = text_table_close_reason(close_reason_stats=results['close_reason_summary'], stake_currency=stake_currency) if isinstance(table, str) and len(table) > 0: print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 00c9b91eb..12e182326 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -45,7 +45,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') min_rate = get_column_def(cols, 'min_rate', 'null') - sell_reason = get_column_def(cols, 'sell_reason', 'null') + close_reason = get_column_def(cols, 'close_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): @@ -58,7 +58,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col close_profit_abs = get_column_def( cols, 'close_profit_abs', f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") - sell_order_status = get_column_def(cols, 'sell_order_status', 'null') + close_order_status = get_column_def(cols, 'close_order_status', 'null') amount_requested = get_column_def(cols, 'amount_requested', 'amount') # Schema migration necessary @@ -80,7 +80,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, sell_order_status, strategy, + max_rate, min_rate, close_reason, close_order_status, strategy, timeframe, open_trade_value, close_profit_abs ) select id, lower(exchange), @@ -101,8 +101,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {initial_stop_loss} initial_stop_loss, {initial_stop_loss_pct} initial_stop_loss_pct, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, - {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, - {sell_order_status} sell_order_status, + {max_rate} max_rate, {min_rate} min_rate, {close_reason} close_reason, + {close_order_status} close_order_status, {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs from {table_back_name} diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index bbbc55c13..35888618d 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -29,13 +29,13 @@ _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database def init_db(db_url: str, clean_open_orders: bool = False) -> None: """ - Initializes this module with the given config, - registers all known command handlers - and starts polling for message updates - :param db_url: Database to use - :param clean_open_orders: Remove open orders from the database. - Useful for dry-run or if all orders have been reset on the exchange. - :return: None + Initializes this module with the given config, + registers all known command handlers + and starts polling for message updates + :param db_url: Database to use + :param clean_open_orders: Remove open orders from the database. + Useful for dry-run or if all orders have been reset on the exchange. + :return: None """ kwargs = {} @@ -131,9 +131,10 @@ class Order(_DECL_BASE): order_date = Column(DateTime, nullable=True, default=datetime.utcnow) order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) + + leverage = Column(Float, nullable=True) def __repr__(self): - return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' f'side={self.side}, order_type={self.order_type}, status={self.status})') @@ -155,6 +156,7 @@ class Order(_DECL_BASE): self.average = order.get('average', self.average) self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) + self.leverage = order.get('leverage', self.leverage) if 'timestamp' in order and order['timestamp'] is not None: self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) @@ -226,6 +228,7 @@ class LocalTrade(): fee_close_currency: str = '' open_rate: float = 0.0 open_rate_requested: Optional[float] = None + # open_trade_value - calculated via _calc_open_trade_value open_trade_value: float = 0.0 close_rate: Optional[float] = None @@ -254,11 +257,20 @@ class LocalTrade(): max_rate: float = 0.0 # Lowest price reached min_rate: float = 0.0 - sell_reason: str = '' - sell_order_status: str = '' + close_reason: str = '' + close_order_status: str = '' strategy: str = '' timeframe: Optional[int] = None + #Margin trading properties + leverage: float = 1.0 + borrowed: float = 0 + borrowed_currency: float = None + interest_rate: float = 0 + min_stoploss: float = None + isShort: boolean = False + #End of margin trading properties + def __init__(self, **kwargs): for key in kwargs: setattr(self, key, kwargs[key]) @@ -322,8 +334,8 @@ class LocalTrade(): 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_abs': self.close_profit_abs, - 'sell_reason': self.sell_reason, - 'sell_order_status': self.sell_order_status, + 'close_reason': self.close_reason, + 'close_order_status': self.close_order_status, 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, @@ -340,6 +352,13 @@ class LocalTrade(): 'min_rate': self.min_rate, 'max_rate': self.max_rate, + 'leverage': self.leverage, + 'borrowed': self.borrowed, + 'borrowed_currency': self.borrowed_currency, + 'interest_rate': self.interest_rate, + 'min_stoploss': self.min_stoploss, + 'leverage': self.leverage, + 'open_order_id': self.open_order_id, } @@ -379,6 +398,9 @@ class LocalTrade(): return new_loss = float(current_price * (1 - abs(stoploss))) + #TODO: Could maybe move this if into the new stoploss if branch + if (self.min_stoploss): #If trading on margin, don't set the stoploss below the liquidation price + new_loss = min(self.min_stoploss, new_loss) # no stop loss assigned yet if not self.stop_loss: @@ -389,7 +411,7 @@ class LocalTrade(): # evaluate if the stop loss needs to be updated else: - if new_loss > self.stop_loss: # stop losses only walk up, never down! + if (new_loss > self.stop_loss and not self.isShort) or (new_loss < self.stop_loss and self.isShort): # stop losses only walk up, never down!, #TODO: But adding more to a margin account would create a lower liquidation price, decreasing the minimum stoploss logger.debug(f"{self.pair} - Adjusting stoploss...") self._set_new_stoploss(new_loss, stoploss) else: @@ -403,6 +425,20 @@ class LocalTrade(): f"Trailing stoploss saved us: " f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") + def is_opening_trade(self, side) -> bool: + """ + Determines if the trade is an opening (long buy or short sell) trade + :param side (string): the side (buy/sell) that order happens on + """ + return (side == 'buy' and not self.isShort) or (side == 'sell' and self.isShort) + + def is_closing_trade(self, side) -> bool: + """ + Determines if the trade is an closing (long sell or short buy) trade + :param side (string): the side (buy/sell) that order happens on + """ + return (side == 'sell' and not self.isShort) or (side == 'buy' and self.isShort) + def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates. @@ -416,22 +452,24 @@ class LocalTrade(): logger.info('Updating trade (id=%s) ...', self.id) - if order_type in ('market', 'limit') and order['side'] == 'buy': + if order_type in ('market', 'limit') and self.isOpeningTrade(order['side']): # Update open rate and actual amount self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_value() if self.is_open: - logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') + payment = "SELL" if self.isShort else "BUY" + logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') self.open_order_id = None - elif order_type in ('market', 'limit') and order['side'] == 'sell': + elif order_type in ('market', 'limit') and self.isClosingTrade(order['side']): if self.is_open: - logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') - self.close(safe_value_fallback(order, 'average', 'price')) + payment = "BUY" if self.isShort else "SELL" + logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') + self.close(safe_value_fallback(order, 'average', 'price')) #TODO: Double check this elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss - self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value + self.close_reason = SellType.STOPLOSS_ON_EXCHANGE.value if self.is_open: logger.info(f'{order_type.upper()} is hit for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) @@ -445,11 +483,11 @@ class LocalTrade(): and marks trade as closed """ self.close_rate = rate + self.close_date = self.close_date or datetime.utcnow() self.close_profit = self.calc_profit_ratio() self.close_profit_abs = self.calc_profit() - self.close_date = self.close_date or datetime.utcnow() self.is_open = False - self.sell_order_status = 'closed' + self.close_order_status = 'closed' self.open_order_id = None if show_msg: logger.info( @@ -462,14 +500,14 @@ class LocalTrade(): """ Update Fee parameters. Only acts once per side """ - if side == 'buy' and self.fee_open_currency is None: + if self.is_opening_trade(side) and self.fee_open_currency is None: self.fee_open_cost = fee_cost self.fee_open_currency = fee_currency if fee_rate is not None: self.fee_open = fee_rate # Assume close-fee will fall into the same fee category and take an educated guess self.fee_close = fee_rate - elif side == 'sell' and self.fee_close_currency is None: + elif self.is_closing_trade(side) and self.fee_close_currency is None: self.fee_close_cost = fee_cost self.fee_close_currency = fee_currency if fee_rate is not None: @@ -479,9 +517,9 @@ class LocalTrade(): """ Verify if this side (buy / sell) has already been updated """ - if side == 'buy': + if self.is_opening_trade(side): return self.fee_open_currency is not None - elif side == 'sell': + elif self.is_closing_trade(side): return self.fee_close_currency is not None else: return False @@ -494,9 +532,13 @@ class LocalTrade(): Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ - buy_trade = Decimal(self.amount) * Decimal(self.open_rate) - fees = buy_trade * Decimal(self.fee_open) - return float(buy_trade + fees) + open_trade = Decimal(self.amount) * Decimal(self.open_rate) + fees = open_trade * Decimal(self.fee_open) + if (self.isShort): + return float(open_trade - fees) + else: + return float(open_trade + fees) + def recalc_open_trade_value(self) -> None: """ @@ -513,34 +555,47 @@ class LocalTrade(): If rate is not set self.fee will be used :param rate: rate to compare with (optional). If rate is not set self.close_rate will be used + :param borrowed: amount borrowed to make this trade + If borrowed is not set self.borrowed will be used :return: Price in BTC of the open trade """ if rate is None and not self.close_rate: return 0.0 - sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore - fees = sell_trade * Decimal(fee or self.fee_close) - return float(sell_trade - fees) + close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore + fees = close_trade * Decimal(fee or self.fee_close) + if (self.isShort): + interest = (self.interest_rate * Decimal(borrowed or self.borrowed)) * (datetime.utcnow() - self.open_date).days #Interest/day * num of days, 0 if not margin + return float(close_trade + fees + interest) + else: + return float(close_trade - fees) def calc_profit(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: """ Calculate the absolute profit in stake currency between Close and Open trade :param fee: fee to use on the close rate (optional). - If rate is not set self.fee will be used + If fee is not set self.fee will be used :param rate: close rate to compare with (optional). If rate is not set self.close_rate will be used + :param borrowed: amount borrowed to make this trade + If borrowed is not set self.borrowed will be used :return: profit in stake currency as float """ close_trade_value = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit = close_trade_value - self.open_trade_value + + if self.isShort: + profit = self.open_trade_value - close_trade_value + else: + profit = close_trade_value - self.open_trade_value return float(f"{profit:.8f}") def calc_profit_ratio(self, rate: Optional[float] = None, - fee: Optional[float] = None) -> float: + fee: Optional[float] = None, + borrowed: Optional[float] = None) -> float: """ Calculates the profit as ratio (including fee). :param rate: rate to compare with (optional). @@ -554,7 +609,10 @@ class LocalTrade(): ) if self.open_trade_value == 0.0: return 0.0 - profit_ratio = (close_trade_value / self.open_trade_value) - 1 + if self.isShort: + profit_ratio = (close_trade_value / self.open_trade_value) - 1 + else: + profit_ratio = (self.open_trade_value / close_trade_value) - 1 return float(f"{profit_ratio:.8f}") def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: @@ -604,7 +662,7 @@ class LocalTrade(): sel_trades = [trade for trade in sel_trades if trade.close_date and trade.close_date > close_date] - return sel_trades + return sel_trades #TODO: What is sel_trades does it mean sell_trades? If so, update this for margin @staticmethod def close_bt_trade(trade): @@ -700,8 +758,8 @@ class Trade(_DECL_BASE, LocalTrade): max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached min_rate = Column(Float, nullable=True) - sell_reason = Column(String(100), nullable=True) - sell_order_status = Column(String(100), nullable=True) + close_reason = Column(String(100), nullable=True) + close_order_status = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index c1b1232c2..b45f7410a 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -193,7 +193,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: if trades is not None and len(trades) > 0: # Create description for sell summarizing the trade trades['desc'] = trades.apply(lambda row: f"{round(row['profit_ratio'] * 100, 1)}%, " - f"{row['sell_reason']}, " + f"{row['close_reason']}, " f"{row['trade_duration']} min", axis=1) trade_buys = go.Scatter( diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 45d393411..ed191ca6b 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -44,8 +44,8 @@ class StoplossGuard(IProtection): # filters = [ # Trade.is_open.is_(False), # Trade.close_date > look_back_until, - # or_(Trade.sell_reason == SellType.STOP_LOSS.value, - # and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value, + # or_(Trade.close_reason == SellType.STOP_LOSS.value, + # and_(Trade.close_reason == SellType.TRAILING_STOP_LOSS.value, # Trade.close_profit < 0)) # ] # if pair: @@ -53,7 +53,7 @@ class StoplossGuard(IProtection): # trades = Trade.get_trades(filters).all() trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) - trades = [trade for trade in trades1 if (str(trade.sell_reason) in ( + trades = [trade for trade in trades1 if (str(trade.close_reason) in ( SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, SellType.STOPLOSS_ON_EXCHANGE.value) and trade.close_profit and trade.close_profit < 0)] diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 4d06d3ecf..2d96184b3 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -94,7 +94,7 @@ class SellReason(BaseModel): class Stats(BaseModel): - sell_reasons: Dict[str, SellReason] + close_reasons: Dict[str, SellReason] durations: Dict[str, Union[str, float]] @@ -168,8 +168,8 @@ class TradeSchema(BaseModel): profit_pct: Optional[float] profit_abs: Optional[float] profit_fiat: Optional[float] - sell_reason: Optional[str] - sell_order_status: Optional[str] + close_reason: Optional[str] + close_order_status: Optional[str] stop_loss_abs: Optional[float] stop_loss_ratio: Optional[float] stop_loss_pct: Optional[float] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2a7721af0..46d8118c1 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -315,11 +315,11 @@ class RPC: return 'draws' trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) # Sell reason - sell_reasons = {} + close_reasons = {} for trade in trades: - if trade.sell_reason not in sell_reasons: - sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0} - sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 + if trade.close_reason not in close_reasons: + close_reasons[trade.close_reason] = {'wins': 0, 'losses': 0, 'draws': 0} + close_reasons[trade.close_reason][trade_win_loss(trade)] += 1 # Duration dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []} @@ -333,7 +333,7 @@ class RPC: losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A' durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} - return {'sell_reasons': sell_reasons, 'durations': durations} + return {'close_reasons': close_reasons, 'durations': durations} def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str, @@ -539,8 +539,8 @@ class RPC: if not fully_canceled: # Get current rate and execute sell current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False) - sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) - self._freqtrade.execute_sell(trade, current_rate, sell_reason) + close_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) + self._freqtrade.execute_sell(trade, current_rate, close_reason) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index aee513017..6c97b3cda 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -230,7 +230,7 @@ class Telegram(RPCHandler): message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" - "*Sell Reason:* `{sell_reason}`\n" + "*Sell Reason:* `{close_reason}`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" @@ -253,7 +253,7 @@ class Telegram(RPCHandler): if isinstance(sell_noti, str): noti = sell_noti else: - noti = sell_noti.get(str(msg['sell_reason']), default_noti) + noti = sell_noti.get(str(msg['close_reason']), default_noti) else: noti = self._config['telegram'] \ .get('notification_settings', {}).get(str(msg_type), default_noti) @@ -306,7 +306,7 @@ class Telegram(RPCHandler): return "\N{ROCKET}" elif float(msg['profit_percent']) >= 0.0: return "\N{EIGHT SPOKED ASTERISK}" - elif msg['sell_reason'] == "stop_loss": + elif msg['close_reason'] == "stop_loss": return"\N{WARNING SIGN}" else: return "\N{CROSS MARK}" @@ -360,8 +360,8 @@ class Telegram(RPCHandler): lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " "`({stoploss_current_dist_pct:.2f}%)`") if r['open_order']: - if r['sell_order_status']: - lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") + if r['close_order_status']: + lines.append("*Open Order:* `{open_order}` - `{close_order_status}`") else: lines.append("*Open Order:* `{open_order}`") @@ -538,16 +538,16 @@ class Telegram(RPCHandler): 'force_sell': 'Forcesell', 'emergency_sell': 'Emergency Sell', } - sell_reasons_tabulate = [ + close_reasons_tabulate = [ [ reason_map.get(reason, reason), sum(count.values()), count['wins'], count['losses'] - ] for reason, count in stats['sell_reasons'].items() + ] for reason, count in stats['close_reasons'].items() ] - sell_reasons_msg = tabulate( - sell_reasons_tabulate, + close_reasons_msg = tabulate( + close_reasons_tabulate, headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] ) durations = stats['durations'] @@ -559,7 +559,7 @@ class Telegram(RPCHandler): ], headers=['', 'Avg. Duration'] ) - msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") + msg = (f"""```\n{close_reasons_msg}```\n```\n{duration_msg}```""") self._send_msg(msg, ParseMode.MARKDOWN) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 8ea38f503..b03003728 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -32,11 +32,11 @@ class SellCheckTuple(object): NamedTuple for Sell type + reason """ sell_type: SellType - sell_reason: str = '' + close_reason: str = '' - def __init__(self, sell_type: SellType, sell_reason: str = ''): + def __init__(self, sell_type: SellType, close_reason: str = ''): self.sell_type = sell_type - self.sell_reason = sell_reason or sell_type.value + self.close_reason = close_reason or sell_type.value @property def sell_flag(self): @@ -225,7 +225,7 @@ class IStrategy(ABC, HyperStrategyMixin): return True def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, + rate: float, time_in_force: str, close_reason: str, current_time: datetime, **kwargs) -> bool: """ Called right before placing a regular sell order. @@ -242,7 +242,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param amount: Amount in quote currency. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). - :param sell_reason: Sell reason. + :param close_reason: Sell reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] :param current_time: datetime object, containing the current datetime @@ -586,7 +586,7 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug(f"{trade.pair} - Sell signal received. " f"sell_type=SellType.{sell_signal.name}" + (f", custom_reason={custom_reason}" if custom_reason else "")) - return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason) + return SellCheckTuple(sell_type=sell_signal, close_reason=custom_reason) if stoplossflag.sell_flag: diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 0bc593e2d..0455cda4e 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -185,7 +185,7 @@ "trades = load_backtest_data(backtest_dir)\n", "\n", "# Show value-counts per pair\n", - "trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" + "trades.groupby(\"pair\")[\"close_reason\"].value_counts()" ] }, { @@ -209,7 +209,7 @@ "trades = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", "\n", "# Display results\n", - "trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" + "trades.groupby(\"pair\")[\"close_reason\"].value_counts()" ] }, { diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 2a9ac0690..e9bb31978 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -62,7 +62,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f return True def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, + rate: float, time_in_force: str, close_reason: str, current_time: 'datetime', **kwargs) -> bool: """ Called right before placing a regular sell order. @@ -79,7 +79,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: :param amount: Amount in quote currency. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). - :param sell_reason: Sell reason. + :param close_reason: Sell reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] :param current_time: datetime object, containing the current datetime