Added sell_tag and buy/sell telegram performance functions

This commit is contained in:
theluxaz 2021-10-13 00:02:28 +03:00
parent 0a52d7c24f
commit b898f86364
11 changed files with 1673 additions and 18 deletions

View File

@ -30,7 +30,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'fee_open', 'fee_close', 'trade_duration', 'fee_open', 'fee_close', 'trade_duration',
'profit_ratio', 'profit_abs', 'sell_reason', 'profit_ratio', 'profit_abs', 'sell_reason',
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag'] 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag', 'sell_tag']
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:

View File

@ -14,3 +14,4 @@ class SignalTagType(Enum):
Enum for signal columns Enum for signal columns
""" """
BUY_TAG = "buy_tag" BUY_TAG = "buy_tag"
SELL_TAG = "sell_tag"

View File

@ -420,7 +420,7 @@ class FreqtradeBot(LoggingMixin):
return False return False
# running get_signal on historical data fetched # running get_signal on historical data fetched
(buy, sell, buy_tag) = self.strategy.get_signal( (buy, sell, buy_tag,sell_tag) = self.strategy.get_signal(
pair, pair,
self.strategy.timeframe, self.strategy.timeframe,
analyzed_df analyzed_df
@ -706,7 +706,7 @@ class FreqtradeBot(LoggingMixin):
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
self.strategy.timeframe) self.strategy.timeframe)
(buy, sell, _) = self.strategy.get_signal( (buy, sell, buy_tag, sell_tag) = self.strategy.get_signal(
trade.pair, trade.pair,
self.strategy.timeframe, self.strategy.timeframe,
analyzed_df analyzed_df
@ -714,7 +714,7 @@ class FreqtradeBot(LoggingMixin):
logger.debug('checking sell') logger.debug('checking sell')
sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
if self._check_and_execute_sell(trade, sell_rate, buy, sell): if self._check_and_execute_sell(trade, sell_rate, buy, sell, sell_tag):
return True return True
logger.debug('Found no sell signal for %s.', trade) logger.debug('Found no sell signal for %s.', trade)
@ -852,18 +852,19 @@ class FreqtradeBot(LoggingMixin):
f"for pair {trade.pair}.") f"for pair {trade.pair}.")
def _check_and_execute_sell(self, trade: Trade, sell_rate: float, def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
buy: bool, sell: bool) -> bool: buy: bool, sell: bool, sell_tag: Optional[str]) -> bool:
""" """
Check and execute sell Check and execute sell
""" """
print(str(sell_tag)+"1")
should_sell = self.strategy.should_sell( should_sell = self.strategy.should_sell(
trade, sell_rate, datetime.now(timezone.utc), buy, sell, trade, sell_rate, datetime.now(timezone.utc), buy, sell,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
) )
if should_sell.sell_flag: if should_sell.sell_flag:
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. Tag: {sell_tag}')
self.execute_trade_exit(trade, sell_rate, should_sell) self.execute_trade_exit(trade, sell_rate, should_sell,sell_tag)
return True return True
return False return False
@ -1064,7 +1065,7 @@ class FreqtradeBot(LoggingMixin):
raise DependencyException( raise DependencyException(
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple, sell_tag: Optional[str] = None) -> bool:
""" """
Executes a trade exit for the given trade and limit Executes a trade exit for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
@ -1141,6 +1142,7 @@ class FreqtradeBot(LoggingMixin):
trade.sell_order_status = '' trade.sell_order_status = ''
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.sell_reason = sell_reason.sell_reason trade.sell_reason = sell_reason.sell_reason
trade.sell_tag = sell_tag
# In case of market sell orders the order can be closed immediately # In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') in ('closed', 'expired'): if order.get('status', 'unknown') in ('closed', 'expired'):
self.update_trade_state(trade, trade.open_order_id, order) self.update_trade_state(trade, trade.open_order_id, order)

View File

@ -44,7 +44,7 @@ SELL_IDX = 4
LOW_IDX = 5 LOW_IDX = 5
HIGH_IDX = 6 HIGH_IDX = 6
BUY_TAG_IDX = 7 BUY_TAG_IDX = 7
SELL_TAG_IDX = 8
class Backtesting: class Backtesting:
""" """
@ -218,7 +218,7 @@ class Backtesting:
""" """
# Every change to this headers list must evaluate further usages of the resulting tuple # Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top # and eventually change the constants for indexes at the top
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag'] headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'sell_tag']
data: Dict = {} data: Dict = {}
self.progress.init_step(BacktestState.CONVERT, len(processed)) self.progress.init_step(BacktestState.CONVERT, len(processed))
@ -230,6 +230,7 @@ class Backtesting:
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist 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[:, 'sell'] = 0 # cleanup if sell_signal is exist
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag 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
df_analyzed = self.strategy.advise_sell( df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy() self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy()
@ -241,6 +242,7 @@ class Backtesting:
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].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[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
df_analyzed.loc[:, 'sell_tag'] = df_analyzed.loc[:, 'sell_tag'].shift(1)
# Update dataprovider cache # Update dataprovider cache
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
@ -319,6 +321,9 @@ class Backtesting:
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
sell_candle_time, sell_row[BUY_IDX], sell_candle_time, sell_row[BUY_IDX],
@ -327,6 +332,8 @@ class Backtesting:
if sell.sell_flag: if sell.sell_flag:
trade.close_date = sell_candle_time trade.close_date = sell_candle_time
if(sell_row[SELL_TAG_IDX] is not None):
trade.sell_tag = sell_row[SELL_TAG_IDX]
trade.sell_reason = sell.sell_reason trade.sell_reason = sell.sell_reason
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) 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) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
@ -375,6 +382,7 @@ class Backtesting:
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
# Enter trade # Enter trade
has_buy_tag = len(row) >= BUY_TAG_IDX + 1 has_buy_tag = len(row) >= BUY_TAG_IDX + 1
has_sell_tag = len(row) >= SELL_TAG_IDX + 1
trade = LocalTrade( trade = LocalTrade(
pair=pair, pair=pair,
open_rate=row[OPEN_IDX], open_rate=row[OPEN_IDX],
@ -385,6 +393,7 @@ class Backtesting:
fee_close=self.fee, fee_close=self.fee,
is_open=True, is_open=True,
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
sell_tag=row[SELL_TAG_IDX] if has_sell_tag else None,
exchange='backtesting', exchange='backtesting',
) )
return trade return trade

View File

@ -82,7 +82,7 @@ def _generate_result_line(result: DataFrame, starting_balance: int, first_column
'profit_sum_pct': round(profit_sum * 100.0, 2), 'profit_sum_pct': round(profit_sum * 100.0, 2),
'profit_total_abs': result['profit_abs'].sum(), 'profit_total_abs': result['profit_abs'].sum(),
'profit_total': profit_total, 'profit_total': profit_total,
'profit_total_pct': round(profit_total * 100.0, 2), 'profit_total_pct': round(profit_sum * 100.0, 2),
'duration_avg': str(timedelta( 'duration_avg': str(timedelta(
minutes=round(result['trade_duration'].mean())) minutes=round(result['trade_duration'].mean()))
) if not result.empty else '0:00', ) if not result.empty else '0:00',
@ -126,6 +126,92 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b
tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
return tabular_data 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]:
"""
Generates and returns a list of metrics for the given tag trades and the results dataframe
:param data: Dict of <pair: dataframe> 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
:return: List of Dicts containing the metrics per pair
"""
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():
continue
tabular_data.append(_generate_tag_result_line(result, starting_balance, tag))
# Sort by total profit %:
tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
# Append Total
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.
"""
profit_sum = result['profit_ratio'].sum()
# (end-capital - starting capital) / starting capital
profit_total = result['profit_abs'].sum() / starting_balance
return {
'key': first_column,
'trades': len(result),
'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0,
'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0,
'profit_sum': profit_sum,
'profit_sum_pct': round(profit_sum * 100.0, 2),
'profit_total_abs': result['profit_abs'].sum(),
'profit_total': profit_total,
'profit_total_pct': round(profit_total * 100.0, 2),
'duration_avg': str(timedelta(
minutes=round(result['trade_duration'].mean()))
) if not result.empty else '0:00',
# 'duration_max': str(timedelta(
# minutes=round(result['trade_duration'].max()))
# ) if not result.empty else '0:00',
# 'duration_min': str(timedelta(
# minutes=round(result['trade_duration'].min()))
# ) if not result.empty else '0:00',
'wins': len(result[result['profit_abs'] > 0]),
'draws': len(result[result['profit_abs'] == 0]),
'losses': len(result[result['profit_abs'] < 0]),
}
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
""" """
@ -313,6 +399,13 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
starting_balance=starting_balance, starting_balance=starting_balance,
results=results, skip_nan=False) 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)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
results=results) results=results)
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
@ -336,6 +429,8 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
'best_pair': best_pair, 'best_pair': best_pair,
'worst_pair': worst_pair, 'worst_pair': worst_pair,
'results_per_pair': pair_results, 'results_per_pair': pair_results,
'results_per_buy_tag': buy_tag_results,
'results_per_sell_tag': sell_tag_results,
'sell_reason_summary': sell_reason_stats, 'sell_reason_summary': sell_reason_stats,
'left_open_trades': left_open_results, 'left_open_trades': left_open_results,
'total_trades': len(results), 'total_trades': len(results),
@ -504,6 +599,27 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
] for t in sell_reason_stats] ] for t in sell_reason_stats]
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") 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:
"""
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
"""
headers = _get_line_header("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]
# 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: def text_table_strategy(strategy_results, stake_currency: str) -> str:
""" """
@ -624,12 +740,24 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table) print(table)
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_sell_reason(sell_reason_stats=results['sell_reason_summary'], table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
stake_currency=stake_currency) stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0: if isinstance(table, str) and len(table) > 0:
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table) print(table)
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0: if isinstance(table, str) and len(table) > 0:
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
@ -640,8 +768,16 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '='))
print(table) print(table)
table = text_table_tags("sell_tag",results['results_per_sell_tag'], stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0:
print(' SELL TAG STATS '.center(len(table.splitlines()[0]), '='))
print(table)
if isinstance(table, str) and len(table) > 0: if isinstance(table, str) and len(table) > 0:
print('=' * len(table.splitlines()[0])) print('=' * len(table.splitlines()[0]))
print() print()

View File

@ -82,7 +82,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, stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update, stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, sell_tag,
timeframe, open_trade_value, close_profit_abs timeframe, open_trade_value, close_profit_abs
) )
select id, lower(exchange), pair, select id, lower(exchange), pair,
@ -98,7 +98,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, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
{sell_order_status} sell_order_status, {sell_order_status} sell_order_status,
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe, {strategy} strategy, {buy_tag} buy_tag, {sell_tag} sell_tag, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
from {table_back_name} from {table_back_name}
""")) """))
@ -157,7 +157,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
table_back_name = get_backup_name(tabs, 'trades_bak') table_back_name = get_backup_name(tabs, 'trades_bak')
# Check for latest column # Check for latest column
if not has_column(cols, 'buy_tag'): if not has_column(cols, 'sell_tag'):
logger.info(f'Running database migration for trades - backup: {table_back_name}') logger.info(f'Running database migration for trades - backup: {table_back_name}')
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
# Reread columns - the above recreated the table! # Reread columns - the above recreated the table!

View File

@ -258,6 +258,7 @@ class LocalTrade():
sell_order_status: str = '' sell_order_status: str = ''
strategy: str = '' strategy: str = ''
buy_tag: Optional[str] = None buy_tag: Optional[str] = None
sell_tag: Optional[str] = None
timeframe: Optional[int] = None timeframe: Optional[int] = None
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -324,7 +325,8 @@ class LocalTrade():
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
'profit_abs': self.close_profit_abs, 'profit_abs': self.close_profit_abs,
'sell_reason': self.sell_reason, 'sell_reason': (f' ({self.sell_reason})' if self.sell_reason else ''), #+str(self.sell_reason) ## CHANGE TO BUY TAG IF NEEDED
'sell_tag': self.sell_tag,
'sell_order_status': self.sell_order_status, 'sell_order_status': self.sell_order_status,
'stop_loss_abs': self.stop_loss, 'stop_loss_abs': self.stop_loss,
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
@ -706,6 +708,7 @@ class Trade(_DECL_BASE, LocalTrade):
sell_order_status = Column(String(100), nullable=True) sell_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True)
buy_tag = Column(String(100), nullable=True) buy_tag = Column(String(100), nullable=True)
sell_tag = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True) timeframe = Column(Integer, nullable=True)
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -856,6 +859,122 @@ class Trade(_DECL_BASE, LocalTrade):
for pair, profit, profit_abs, count in pair_rates for pair, profit, profit_abs, count in pair_rates
] ]
@staticmethod
def get_buy_tag_performance(pair: str) -> List[Dict[str, Any]]:
"""
Returns List of dicts containing all Trades, based on buy tag performance
Can either be average for all pairs or a specific pair provided
NOTE: Not supported in Backtesting.
"""
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()
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()
return [
{
'buy_tag': buy_tag,
'profit': profit,
'profit_abs': profit_abs,
'count': count
}
for buy_tag, profit, profit_abs, count in tag_perf
]
@staticmethod
def get_sell_tag_performance(pair: str) -> List[Dict[str, Any]]:
"""
Returns List of dicts containing all Trades, based on sell 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()
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()
return [
{
'sell_tag': sell_tag,
'profit': profit,
'profit_abs': profit_abs,
'count': count
}
for sell_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
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()
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()
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
]
@staticmethod @staticmethod
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)): def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
""" """

View File

@ -669,6 +669,37 @@ class RPC:
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates] [x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates]
return pair_rates return pair_rates
def _rpc_buy_tag_performance(self, pair: str) -> List[Dict[str, Any]]:
"""
Handler for buy tag performance.
Shows a performance statistic from finished trades
"""
buy_tags = Trade.get_buy_tag_performance(pair)
# Round and convert to %
[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]]:
"""
Handler for sell tag performance.
Shows a performance statistic from finished trades
"""
sell_tags = Trade.get_sell_tag_performance(pair)
# Round and convert to %
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in sell_tags]
return sell_tags
def _rpc_mix_tag_performance(self, pair: str) -> List[Dict[str, Any]]:
"""
Handler for mix tag performance.
Shows a performance statistic from finished trades
"""
mix_tags = Trade.get_mix_tag_performance(pair)
# Round and convert to %
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in mix_tags]
return mix_tags
def _rpc_count(self) -> Dict[str, float]: def _rpc_count(self) -> Dict[str, float]:
""" Returns the number of trades running """ """ Returns the number of trades running """
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:

View File

@ -108,6 +108,7 @@ class Telegram(RPCHandler):
r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$', r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$',
r'/profit$', r'/profit \d+', r'/profit$', r'/profit \d+',
r'/stats$', r'/count$', r'/locks$', r'/balance$', r'/stats$', r'/count$', r'/locks$', r'/balance$',
r'/buys',r'/sells',r'/mix_tags',
r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/stopbuy$', r'/reload_config$', r'/show_config$',
r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$',
r'/forcebuy$', r'/help$', r'/version$'] r'/forcebuy$', r'/help$', r'/version$']
@ -152,6 +153,9 @@ class Telegram(RPCHandler):
CommandHandler('trades', self._trades), CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade), CommandHandler('delete', self._delete_trade),
CommandHandler('performance', self._performance), CommandHandler('performance', self._performance),
CommandHandler('buys', self._buy_tag_performance),
CommandHandler('sells', self._sell_tag_performance),
CommandHandler('mix_tags', self._mix_tag_performance),
CommandHandler('stats', self._stats), CommandHandler('stats', self._stats),
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
@ -173,6 +177,9 @@ class Telegram(RPCHandler):
CallbackQueryHandler(self._profit, pattern='update_profit'), CallbackQueryHandler(self._profit, pattern='update_profit'),
CallbackQueryHandler(self._balance, pattern='update_balance'), CallbackQueryHandler(self._balance, pattern='update_balance'),
CallbackQueryHandler(self._performance, pattern='update_performance'), 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_mix_tag_performance'),
CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._count, pattern='update_count'),
CallbackQueryHandler(self._forcebuy_inline), CallbackQueryHandler(self._forcebuy_inline),
] ]
@ -258,6 +265,42 @@ class Telegram(RPCHandler):
"*Current Rate:* `{current_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n"
"*Close Rate:* `{limit:.8f}`").format(**msg) "*Close Rate:* `{limit:.8f}`").format(**msg)
sell_tag = msg['sell_tag']
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 return message
def send_msg(self, msg: Dict[str, Any]) -> None: def send_msg(self, msg: Dict[str, Any]) -> None:
@ -364,6 +407,7 @@ class Telegram(RPCHandler):
"*Current Pair:* {pair}", "*Current Pair:* {pair}",
"*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Amount:* `{amount} ({stake_amount} {base_currency})`",
"*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "",
"*Sell Tag:* `{sell_tag}`" if r['sell_tag'] else "",
"*Open Rate:* `{open_rate:.8f}`", "*Open Rate:* `{open_rate:.8f}`",
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
"*Current Rate:* `{current_rate:.8f}`", "*Current Rate:* `{current_rate:.8f}`",
@ -845,6 +889,111 @@ class Telegram(RPCHandler):
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
@authorized_only
def _buy_tag_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /buys PAIR .
Shows a performance statistic from finished trades
:param bot: telegram bot
:param update: message update
:return: None
"""
try:
pair=None
if context.args:
pair = context.args[0]
trades = self._rpc._rpc_buy_tag_performance(pair)
output = "<b>Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (
f"{i+1}.\t <code>{trade['buy_tag']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit']:.2f}%) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_buy_tag_performance",
query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _sell_tag_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /sells.
Shows a performance statistic from finished trades
:param bot: telegram bot
:param update: message update
:return: None
"""
try:
pair=None
if context.args:
pair = context.args[0]
trades = self._rpc._rpc_sell_tag_performance(pair)
output = "<b>Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (
f"{i+1}.\t <code>{trade['sell_tag']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit']:.2f}%) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_sell_tag_performance",
query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /mix_tags.
Shows a performance statistic from finished trades
:param bot: telegram bot
:param update: message update
:return: None
"""
try:
pair=None
if context.args:
pair = context.args[0]
trades = self._rpc._rpc_mix_tag_performance(pair)
output = "<b>Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (
f"{i+1}.\t <code>{trade['mix_tag']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit']:.2f}%) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_mix_tag_performance",
query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@authorized_only @authorized_only
def _count(self, update: Update, context: CallbackContext) -> None: def _count(self, update: Update, context: CallbackContext) -> None:
""" """
@ -1020,6 +1169,9 @@ class Telegram(RPCHandler):
" *table :* `will display trades in a table`\n" " *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an asterisk (*)`\n" " `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n" " `pending sell orders are marked with a double asterisk (**)`\n"
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
"*/mix_tag <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, " "*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
"over the last n days`\n" "over the last n days`\n"

View File

@ -460,6 +460,7 @@ class IStrategy(ABC, HyperStrategyMixin):
dataframe['buy'] = 0 dataframe['buy'] = 0
dataframe['sell'] = 0 dataframe['sell'] = 0
dataframe['buy_tag'] = None dataframe['buy_tag'] = None
dataframe['sell_tag'] = None
# Other Defs in strategy that want to be called every loop here # Other Defs in strategy that want to be called every loop here
# twitter_sell = self.watch_twitter_feed(dataframe, metadata) # twitter_sell = self.watch_twitter_feed(dataframe, metadata)
@ -537,7 +538,7 @@ class IStrategy(ABC, HyperStrategyMixin):
pair: str, pair: str,
timeframe: str, timeframe: str,
dataframe: DataFrame dataframe: DataFrame
) -> Tuple[bool, bool, Optional[str]]: ) -> Tuple[bool, bool, Optional[str], Optional[str]]:
""" """
Calculates current signal based based on the buy / sell columns of the dataframe. Calculates current signal based based on the buy / sell columns of the dataframe.
Used by Bot to get the signal to buy or sell Used by Bot to get the signal to buy or sell
@ -572,6 +573,7 @@ class IStrategy(ABC, HyperStrategyMixin):
sell = latest[SignalType.SELL.value] == 1 sell = latest[SignalType.SELL.value] == 1
buy_tag = latest.get(SignalTagType.BUY_TAG.value, None) buy_tag = latest.get(SignalTagType.BUY_TAG.value, None)
sell_tag = latest.get(SignalTagType.SELL_TAG.value, None)
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'], pair, str(buy), str(sell)) latest['date'], pair, str(buy), str(sell))
@ -580,8 +582,8 @@ class IStrategy(ABC, HyperStrategyMixin):
current_time=datetime.now(timezone.utc), current_time=datetime.now(timezone.utc),
timeframe_seconds=timeframe_seconds, timeframe_seconds=timeframe_seconds,
buy=buy): buy=buy):
return False, sell, buy_tag return False, sell, buy_tag, sell_tag
return buy, sell, buy_tag return buy, sell, buy_tag, sell_tag
def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, def ignore_expired_candle(self, latest_date: datetime, current_time: datetime,
timeframe_seconds: int, buy: bool): timeframe_seconds: int, buy: bool):

File diff suppressed because it is too large Load Diff