From 69a59cdf37e79de1e476dc32d5c9a482bcd9ea51 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Mon, 18 Oct 2021 23:56:41 +0300 Subject: [PATCH] Fixed flake 8, changed sell_tag to exit_tag and fixed telegram functions --- freqtrade/data/btanalysis.py | 2 +- freqtrade/enums/signaltype.py | 2 +- freqtrade/freqtradebot.py | 41 ++++--- freqtrade/optimize/backtesting.py | 21 ++-- freqtrade/optimize/optimize_reports.py | 93 ++++++--------- freqtrade/persistence/migrations.py | 8 +- freqtrade/persistence/models.py | 158 ++++++++++++++----------- freqtrade/rpc/rpc.py | 11 +- freqtrade/rpc/telegram.py | 83 +++++-------- freqtrade/strategy/interface.py | 8 +- 10 files changed, 206 insertions(+), 221 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 82b2bb3a9..3dba635e6 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -30,7 +30,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'fee_open', 'fee_close', 'trade_duration', 'profit_ratio', 'profit_abs', 'sell_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', - 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag', 'sell_tag'] + 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag', 'exit_tag'] def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index 32ac19ba4..4437f49e3 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -14,4 +14,4 @@ class SignalTagType(Enum): Enum for signal columns """ BUY_TAG = "buy_tag" - SELL_TAG = "sell_tag" \ No newline at end of file + EXIT_TAG = "exit_tag" diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d415c9d93..73d9bb382 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -201,11 +201,11 @@ class FreqtradeBot(LoggingMixin): if len(open_trades) != 0: msg = { 'type': RPCMessageType.WARNING, - 'status': f"{len(open_trades)} open trades active.\n\n" - f"Handle these trades manually on {self.exchange.name}, " - f"or '/start' the bot again and use '/stopbuy' " - f"to handle open trades gracefully. \n" - f"{'Trades are simulated.' if self.config['dry_run'] else ''}", + 'status': f"{len(open_trades)} open trades active.\n\n" + f"Handle these trades manually on {self.exchange.name}, " + f"or '/start' the bot again and use '/stopbuy' " + f"to handle open trades gracefully. \n" + f"{'Trades are simulated.' if self.config['dry_run'] else ''}", } self.rpc.send_msg(msg) @@ -420,7 +420,7 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (buy, sell, buy_tag,sell_tag) = self.strategy.get_signal( + (buy, sell, buy_tag, exit_tag) = self.strategy.get_signal( pair, self.strategy.timeframe, analyzed_df @@ -700,15 +700,14 @@ class FreqtradeBot(LoggingMixin): logger.debug('Handling %s ...', trade) (buy, sell) = (False, False) - - sell_tag=None + exit_tag = None if (self.config.get('use_sell_signal', True) or self.config.get('ignore_roi_if_buy_signal', False)): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, self.strategy.timeframe) - (buy, sell, buy_tag, sell_tag) = self.strategy.get_signal( + (buy, sell, buy_tag, exit_tag) = self.strategy.get_signal( trade.pair, self.strategy.timeframe, analyzed_df @@ -716,7 +715,7 @@ class FreqtradeBot(LoggingMixin): logger.debug('checking sell') sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - if self._check_and_execute_exit(trade, sell_rate, buy, sell, sell_tag): + if self._check_and_execute_exit(trade, sell_rate, buy, sell, exit_tag): return True logger.debug('Found no sell signal for %s.', trade) @@ -854,7 +853,7 @@ class FreqtradeBot(LoggingMixin): f"for pair {trade.pair}.") def _check_and_execute_exit(self, trade: Trade, exit_rate: float, - buy: bool, sell: bool, sell_tag: Optional[str]) -> bool: + buy: bool, sell: bool, exit_tag: Optional[str]) -> bool: """ Check and execute exit """ @@ -865,8 +864,9 @@ class FreqtradeBot(LoggingMixin): ) if should_sell.sell_flag: - logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. Tag: {sell_tag if sell_tag is not None else "None"}') - self.execute_trade_exit(trade, exit_rate, should_sell,sell_tag) + logger.info( + f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. Tag: {exit_tag if exit_tag is not None else "None"}') + self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag) return True return False @@ -1067,7 +1067,12 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple, sell_tag: Optional[str] = None) -> bool: + def execute_trade_exit( + self, + trade: Trade, + limit: float, + sell_reason: SellCheckTuple, + exit_tag: Optional[str] = None) -> bool: """ Executes a trade exit for the given trade and limit :param trade: Trade instance @@ -1144,8 +1149,8 @@ class FreqtradeBot(LoggingMixin): trade.sell_order_status = '' trade.close_rate_requested = limit trade.sell_reason = sell_reason.sell_reason - if(sell_tag is not None): - trade.sell_tag = sell_tag + if(exit_tag is not None): + trade.exit_tag = exit_tag # 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) @@ -1187,7 +1192,7 @@ class FreqtradeBot(LoggingMixin): 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, 'sell_reason': trade.sell_reason, - 'sell_tag': trade.sell_tag, + 'exit_tag': trade.exit_tag, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], @@ -1231,7 +1236,7 @@ class FreqtradeBot(LoggingMixin): 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, 'sell_reason': trade.sell_reason, - 'sell_tag': trade.sell_tag, + 'exit_tag': trade.exit_tag, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.now(timezone.utc), 'stake_currency': self.config['stake_currency'], diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 69f2d2580..6c2a20cb1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -44,7 +44,8 @@ SELL_IDX = 4 LOW_IDX = 5 HIGH_IDX = 6 BUY_TAG_IDX = 7 -SELL_TAG_IDX = 8 +EXIT_TAG_IDX = 8 + class Backtesting: """ @@ -247,7 +248,7 @@ class Backtesting: """ # Every change to this headers list must evaluate further usages of the resulting tuple # and eventually change the constants for indexes at the top - headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'sell_tag'] + headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'exit_tag'] data: Dict = {} self.progress.init_step(BacktestState.CONVERT, len(processed)) @@ -259,7 +260,7 @@ class Backtesting: pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist - pair_data.loc[:, 'sell_tag'] = None # cleanup if sell_tag is exist + pair_data.loc[:, 'exit_tag'] = None # cleanup if exit_tag is exist df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy() @@ -271,7 +272,7 @@ class Backtesting: df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) - df_analyzed.loc[:, 'sell_tag'] = df_analyzed.loc[:, 'sell_tag'].shift(1) + df_analyzed.loc[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_tag'].shift(1) # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) @@ -359,8 +360,10 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_candle_time - if(sell_row[SELL_TAG_IDX] is not None): - trade.sell_tag = sell_row[SELL_TAG_IDX] + if(sell_row[EXIT_TAG_IDX] is not None): + trade.exit_tag = sell_row[EXIT_TAG_IDX] + else: + trade.exit_tag = None trade.sell_reason = sell.sell_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) @@ -389,7 +392,7 @@ class Backtesting: detail_data = detail_data.loc[ (detail_data['date'] >= sell_candle_time) & (detail_data['date'] < sell_candle_end) - ].copy() + ].copy() if len(detail_data) == 0: # Fall back to "regular" data if no detail data was found for this candle return self._get_sell_trade_entry_for_candle(trade, sell_row) @@ -435,7 +438,7 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade has_buy_tag = len(row) >= BUY_TAG_IDX + 1 - has_sell_tag = len(row) >= SELL_TAG_IDX + 1 + has_exit_tag = len(row) >= EXIT_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], @@ -446,7 +449,7 @@ class Backtesting: fee_close=self.fee, is_open=True, buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, - sell_tag=row[SELL_TAG_IDX] if has_sell_tag else None, + exit_tag=row[EXIT_TAG_IDX] if has_exit_tag else None, exchange='backtesting', ) return trade diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0e8467788..30005f524 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -54,6 +54,7 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', 'Win Draw Loss Win%'] + def _get_line_header_sell(first_column: str, stake_currency: str) -> List[str]: """ Generate header lines (goes in line with _generate_result_line()) @@ -134,12 +135,13 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data -def generate_tag_metrics(tag_type:str, data: Dict[str, Dict], stake_currency: str, starting_balance: int, - results: DataFrame, skip_nan: bool = False) -> List[Dict]: + +def generate_tag_metrics(tag_type: str, + starting_balance: int, + results: DataFrame, + skip_nan: bool = False) -> List[Dict]: """ Generates and returns a list of metrics for the given tag trades and the results dataframe - :param data: Dict of containing data that was used during backtesting. - :param stake_currency: stake-currency - used to correctly name headers :param starting_balance: Starting balance :param results: Dataframe containing the backtest results :param skip_nan: Print "left open" open trades @@ -148,32 +150,6 @@ def generate_tag_metrics(tag_type:str, data: Dict[str, Dict], stake_currency: st tabular_data = [] - # for tag, count in results[tag_type].value_counts().iteritems(): - # result = results.loc[results[tag_type] == tag] - # - # profit_mean = result['profit_ratio'].mean() - # profit_sum = result['profit_ratio'].sum() - # profit_total = profit_sum / max_open_trades - # - # tabular_data.append( - # { - # 'sell_reason': tag, - # 'trades': count, - # 'wins': len(result[result['profit_abs'] > 0]), - # 'draws': len(result[result['profit_abs'] == 0]), - # 'losses': len(result[result['profit_abs'] < 0]), - # 'profit_mean': profit_mean, - # 'profit_mean_pct': round(profit_mean * 100, 2), - # 'profit_sum': profit_sum, - # 'profit_sum_pct': round(profit_sum * 100, 2), - # 'profit_total_abs': result['profit_abs'].sum(), - # 'profit_total': profit_total, - # 'profit_total_pct': round(profit_total * 100, 2), - # } - # ) - # - # tabular_data = [] - for tag, count in results[tag_type].value_counts().iteritems(): result = results[results[tag_type] == tag] if skip_nan and result['profit_abs'].isnull().all(): @@ -188,6 +164,7 @@ def generate_tag_metrics(tag_type:str, data: Dict[str, Dict], stake_currency: st tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data + def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: """ Generate one result dict, with "first_column" as key. @@ -408,12 +385,10 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], starting_balance=starting_balance, results=results, skip_nan=False) - buy_tag_results = generate_tag_metrics("buy_tag",btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results, skip_nan=False) - sell_tag_results = generate_tag_metrics("sell_tag",btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results, skip_nan=False) + buy_tag_results = generate_tag_metrics("buy_tag", starting_balance=starting_balance, + results=results, skip_nan=False) + exit_tag_results = generate_tag_metrics("exit_tag", starting_balance=starting_balance, + results=results, skip_nan=False) sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, results=results) @@ -439,7 +414,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'worst_pair': worst_pair, 'results_per_pair': pair_results, 'results_per_buy_tag': buy_tag_results, - 'results_per_sell_tag': sell_tag_results, + 'results_per_exit_tag': exit_tag_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), @@ -609,30 +584,38 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren ] for t in sell_reason_stats] return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") -def text_table_tags(tag_type:str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str: + +def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row :param stake_currency: stake-currency - used to correctly name headers :return: pretty printed table with tabulate as string """ - if(tag_type=="buy_tag"): + if(tag_type == "buy_tag"): headers = _get_line_header("TAG", stake_currency) else: headers = _get_line_header_sell("TAG", stake_currency) floatfmt = _get_line_floatfmt(stake_currency) - output = [[ - t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], - _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']) - ] for t in tag_results] + output = [ + [ + t['key'] if t['key'] is not None and len( + t['key']) > 0 else "OTHER", + t['trades'], + t['profit_mean_pct'], + t['profit_sum_pct'], + t['profit_total_abs'], + t['profit_total_pct'], + t['duration_avg'], + _generate_wins_draws_losses( + t['wins'], + t['draws'], + t['losses'])] for t in tag_results] # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") - - def text_table_strategy(strategy_results, stake_currency: str) -> str: """ Generate summary table per strategy @@ -752,14 +735,19 @@ 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_tags("buy_tag", results['results_per_buy_tag'], stake_currency=stake_currency) + table = text_table_tags( + "buy_tag", + results['results_per_buy_tag'], + stake_currency=stake_currency) if isinstance(table, str) and len(table) > 0: print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = text_table_tags("sell_tag",results['results_per_sell_tag'], stake_currency=stake_currency) + table = text_table_tags( + "exit_tag", + results['results_per_exit_tag'], + stake_currency=stake_currency) if isinstance(table, str) and len(table) > 0: print(' SELL TAG STATS '.center(len(table.splitlines()[0]), '=')) @@ -771,10 +759,6 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - - - - table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) if isinstance(table, str) and len(table) > 0: print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) @@ -785,12 +769,9 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) print(table) - - if isinstance(table, str) and len(table) > 0: print('=' * len(table.splitlines()[0])) - print() diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 0f07c13b5..d0b3add3c 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -48,7 +48,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') buy_tag = get_column_def(cols, 'buy_tag', 'null') - sell_tag = get_column_def(cols, 'sell_tag', 'null') + exit_tag = get_column_def(cols, 'exit_tag', 'null') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') @@ -83,7 +83,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, buy_tag, sell_tag, + max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, exit_tag, timeframe, open_trade_value, close_profit_abs ) select id, lower(exchange), pair, @@ -99,7 +99,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {sell_order_status} sell_order_status, - {strategy} strategy, {buy_tag} buy_tag, {sell_tag} sell_tag, {timeframe} timeframe, + {strategy} strategy, {buy_tag} buy_tag, {exit_tag} exit_tag, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs from {table_back_name} """)) @@ -158,7 +158,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: table_back_name = get_backup_name(tabs, 'trades_bak') # Check for latest column - if not has_column(cols, 'sell_tag'): + if not has_column(cols, 'exit_tag'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 33a4429c0..945201982 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -258,7 +258,7 @@ class LocalTrade(): sell_order_status: str = '' strategy: str = '' buy_tag: Optional[str] = None - sell_tag: Optional[str] = None + exit_tag: Optional[str] = None timeframe: Optional[int] = None def __init__(self, **kwargs): @@ -325,8 +325,9 @@ class LocalTrade(): 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_abs': self.close_profit_abs, - 'sell_reason': (f' ({self.sell_reason})' if self.sell_reason else ''), #+str(self.sell_reason) ## CHANGE TO BUY TAG IF NEEDED - 'sell_tag': (f' ({self.sell_tag})' if self.sell_tag else '') , + # +str(self.sell_reason) ## CHANGE TO BUY TAG IF NEEDED + 'sell_reason': (f' ({self.sell_reason})' if self.sell_reason else ''), + 'exit_tag': (f' ({self.exit_tag})' if self.exit_tag else ''), 'sell_order_status': self.sell_order_status, 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, @@ -708,7 +709,7 @@ class Trade(_DECL_BASE, LocalTrade): sell_order_status = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True) buy_tag = Column(String(100), nullable=True) - sell_tag = Column(String(100), nullable=True) + exit_tag = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) def __init__(self, **kwargs): @@ -873,28 +874,28 @@ class Trade(_DECL_BASE, LocalTrade): if(pair is not None): tag_perf = Trade.query.with_entities( - Trade.buy_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .filter(Trade.pair.lower() == pair.lower()) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.buy_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .filter(Trade.pair == pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() else: tag_perf = Trade.query.with_entities( - Trade.buy_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.buy_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.buy_tag) \ + .order_by(desc('profit_sum_abs')) \ + .all() return [ { - 'buy_tag': buy_tag, + 'buy_tag': buy_tag if buy_tag is not None else "Other", 'profit': profit, 'profit_abs': profit_abs, 'count': count @@ -903,81 +904,102 @@ class Trade(_DECL_BASE, LocalTrade): ] @staticmethod - def get_sell_tag_performance(pair: str) -> List[Dict[str, Any]]: + def get_exit_tag_performance(pair: str) -> List[Dict[str, Any]]: """ - Returns List of dicts containing all Trades, based on sell tag performance + Returns List of dicts containing all Trades, based on exit tag performance Can either be average for all pairs or a specific pair provided NOTE: Not supported in Backtesting. """ if(pair is not None): tag_perf = Trade.query.with_entities( - Trade.sell_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .filter(Trade.pair.lower() == pair.lower()) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.exit_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .filter(Trade.pair == pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() else: tag_perf = Trade.query.with_entities( - Trade.sell_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.exit_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.exit_tag) \ + .order_by(desc('profit_sum_abs')) \ + .all() return [ { - 'sell_tag': sell_tag, + 'exit_tag': exit_tag if exit_tag is not None else "Other", 'profit': profit, 'profit_abs': profit_abs, 'count': count } - for sell_tag, profit, profit_abs, count in tag_perf + for exit_tag, profit, profit_abs, count in tag_perf ] @staticmethod def get_mix_tag_performance(pair: str) -> List[Dict[str, Any]]: """ - Returns List of dicts containing all Trades, based on buy_tag + sell_tag performance + Returns List of dicts containing all Trades, based on buy_tag + exit_tag performance Can either be average for all pairs or a specific pair provided NOTE: Not supported in Backtesting. """ if(pair is not None): tag_perf = Trade.query.with_entities( - Trade.buy_tag, - Trade.sell_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .filter(Trade.pair.lower() == pair.lower()) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.id, + Trade.buy_tag, + Trade.exit_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .filter(Trade.pair == pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() + else: tag_perf = Trade.query.with_entities( - Trade.buy_tag, - Trade.sell_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.id, + Trade.buy_tag, + Trade.exit_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.id) \ + .order_by(desc('profit_sum_abs')) \ + .all() - return [ - { 'mix_tag': str(buy_tag) + " " +str(sell_tag), - 'profit': profit, - 'profit_abs': profit_abs, - 'count': count - } - for buy_tag, sell_tag, profit, profit_abs, count in tag_perf - ] + return_list = [] + for id, buy_tag, exit_tag, profit, profit_abs, count in tag_perf: + buy_tag = buy_tag if buy_tag is not None else "Other" + exit_tag = exit_tag if exit_tag is not None else "Other" + + if(exit_tag is not None and buy_tag is not None): + mix_tag = buy_tag + " " + exit_tag + i = 0 + if not any(item["mix_tag"] == mix_tag for item in return_list): + return_list.append({'mix_tag': mix_tag, + 'profit': profit, + 'profit_abs': profit_abs, + 'count': count}) + else: + while i < len(return_list): + if return_list[i]["mix_tag"] == mix_tag: + print("item below") + print(return_list[i]) + return_list[i] = { + 'mix_tag': mix_tag, + 'profit': profit + return_list[i]["profit"], + 'profit_abs': profit_abs + return_list[i]["profit_abs"], + 'count': 1 + return_list[i]["count"]} + i += 1 + + return return_list @staticmethod def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 85973add6..508ce6894 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -105,7 +105,7 @@ class RPC: val = { 'dry_run': config['dry_run'], 'stake_currency': config['stake_currency'], - 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), + 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), 'stake_amount': config['stake_amount'], 'available_capital': config.get('available_capital'), 'max_open_trades': (config['max_open_trades'] @@ -696,16 +696,15 @@ class RPC: [x.update({'profit': round(x['profit'] * 100, 2)}) for x in buy_tags] return buy_tags - - def _rpc_sell_tag_performance(self, pair: str) -> List[Dict[str, Any]]: + def _rpc_exit_tag_performance(self, pair: str) -> List[Dict[str, Any]]: """ Handler for sell tag performance. Shows a performance statistic from finished trades """ - sell_tags = Trade.get_sell_tag_performance(pair) + exit_tags = Trade.get_exit_tag_performance(pair) # Round and convert to % - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in sell_tags] - return sell_tags + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in exit_tags] + return exit_tags def _rpc_mix_tag_performance(self, pair: str) -> List[Dict[str, Any]]: """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index db745ff37..85a91a10e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -108,7 +108,7 @@ class Telegram(RPCHandler): r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+', r'/stats$', r'/count$', r'/locks$', r'/balance$', - r'/buys',r'/sells',r'/mix_tags', + r'/buys', r'/sells', r'/mix_tags', r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$', r'/forcebuy$', r'/help$', r'/version$'] @@ -154,7 +154,7 @@ class Telegram(RPCHandler): CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), CommandHandler('buys', self._buy_tag_performance), - CommandHandler('sells', self._sell_tag_performance), + CommandHandler('sells', self._exit_tag_performance), CommandHandler('mix_tags', self._mix_tag_performance), CommandHandler('stats', self._stats), CommandHandler('daily', self._daily), @@ -178,7 +178,7 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._balance, pattern='update_balance'), CallbackQueryHandler(self._performance, pattern='update_performance'), CallbackQueryHandler(self._performance, pattern='update_buy_tag_performance'), - CallbackQueryHandler(self._performance, pattern='update_sell_tag_performance'), + CallbackQueryHandler(self._performance, pattern='update_exit_tag_performance'), CallbackQueryHandler(self._performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._forcebuy_inline), @@ -242,6 +242,7 @@ class Telegram(RPCHandler): msg['duration'] = msg['close_date'].replace( microsecond=0) - msg['open_date'].replace(microsecond=0) msg['duration_min'] = msg['duration'].total_seconds() / 60 + msg['tags'] = self._get_tags_string(msg) msg['emoji'] = self._get_sell_emoji(msg) @@ -258,6 +259,7 @@ class Telegram(RPCHandler): message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" + "{tags}" "*Sell Reason:* `{sell_reason}`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Amount:* `{amount:.8f}`\n" @@ -265,46 +267,6 @@ class Telegram(RPCHandler): "*Current Rate:* `{current_rate:.8f}`\n" "*Close Rate:* `{limit:.8f}`").format(**msg) - sell_tag =None - if("sell_tag" in msg.keys()): - sell_tag = msg['sell_tag'] - buy_tag =None - if("buy_tag" in msg.keys()): - buy_tag = msg['buy_tag'] - - if sell_tag is not None and buy_tag is not None: - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" - "*Buy Tag:* `{buy_tag}`\n" - "*Sell Tag:* `{sell_tag}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`").format(**msg) - elif sell_tag is None and buy_tag is not None: - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" - "*Buy Tag:* `{buy_tag}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`").format(**msg) - elif sell_tag is not None and buy_tag is None: - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" - "*Sell Tag:* `{sell_tag}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`").format(**msg) - - return message def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: @@ -393,6 +355,18 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" + def _get_tags_string(self, msg): + """ + Get string lines for buy/sell tags to display when a sell is made + """ + tag_lines = "" + + if ("buy_tag" in msg.keys() and msg['buy_tag'] is not None): + tag_lines += ("*Buy Tag:* `{buy_tag}`\n").format(msg['buy_tag']) + if ("exit_tag" in msg.keys() and msg['exit_tag'] is not None): + tag_lines += ("*Sell Tag:* `{exit_tag}`\n").format(msg['exit_tag']) + return tag_lines + @authorized_only def _status(self, update: Update, context: CallbackContext) -> None: """ @@ -425,7 +399,7 @@ class Telegram(RPCHandler): "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", - "*Sell Tag:* `{sell_tag}`" if r['sell_tag'] else "", + "*Sell Tag:* `{exit_tag}`" if r['exit_tag'] else "", "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Current Rate:* `{current_rate:.8f}`", @@ -923,12 +897,12 @@ class Telegram(RPCHandler): :return: None """ try: - pair=None + pair = None if context.args: pair = context.args[0] trades = self._rpc._rpc_buy_tag_performance(pair) - output = "Performance:\n" + output = "Buy Tag Performance:\n" for i, trade in enumerate(trades): stat_line = ( f"{i+1}.\t {trade['buy_tag']}\t" @@ -949,7 +923,7 @@ class Telegram(RPCHandler): self._send_msg(str(e)) @authorized_only - def _sell_tag_performance(self, update: Update, context: CallbackContext) -> None: + def _exit_tag_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /sells. Shows a performance statistic from finished trades @@ -958,15 +932,15 @@ class Telegram(RPCHandler): :return: None """ try: - pair=None + pair = None if context.args: pair = context.args[0] - trades = self._rpc._rpc_sell_tag_performance(pair) - output = "Performance:\n" + trades = self._rpc._rpc_exit_tag_performance(pair) + output = "Sell Tag Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i+1}.\t {trade['sell_tag']}\t" + f"{i+1}.\t {trade['exit_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit']:.2f}%) " f"({trade['count']})\n") @@ -978,7 +952,7 @@ class Telegram(RPCHandler): output += stat_line self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_sell_tag_performance", + reload_able=True, callback_path="update_exit_tag_performance", query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -993,13 +967,14 @@ class Telegram(RPCHandler): :return: None """ try: - pair=None + pair = None if context.args: pair = context.args[0] trades = self._rpc._rpc_mix_tag_performance(pair) - output = "Performance:\n" + output = "Mix Tag Performance:\n" for i, trade in enumerate(trades): + print(str(trade)) stat_line = ( f"{i+1}.\t {trade['mix_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 3c82d4d25..e4bf6ca69 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -500,7 +500,7 @@ class IStrategy(ABC, HyperStrategyMixin): dataframe['buy'] = 0 dataframe['sell'] = 0 dataframe['buy_tag'] = None - dataframe['sell_tag'] = None + dataframe['exit_tag'] = None # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) @@ -613,7 +613,7 @@ class IStrategy(ABC, HyperStrategyMixin): sell = latest[SignalType.SELL.value] == 1 buy_tag = latest.get(SignalTagType.BUY_TAG.value, None) - sell_tag = latest.get(SignalTagType.SELL_TAG.value, None) + exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None) logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) @@ -622,8 +622,8 @@ class IStrategy(ABC, HyperStrategyMixin): current_time=datetime.now(timezone.utc), timeframe_seconds=timeframe_seconds, buy=buy): - return False, sell, buy_tag, sell_tag - return buy, sell, buy_tag, sell_tag + return False, sell, buy_tag, exit_tag + return buy, sell, buy_tag, exit_tag def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, timeframe_seconds: int, buy: bool):