Fixed flake 8, changed sell_tag to exit_tag and fixed telegram functions

This commit is contained in:
theluxaz 2021-10-18 23:56:41 +03:00
parent 0bb7ea10ab
commit 69a59cdf37
10 changed files with 206 additions and 221 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = "<b>Performance:</b>\n"
output = "<b>Buy Tag Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (
f"{i+1}.\t <code>{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 = "<b>Performance:</b>\n"
trades = self._rpc._rpc_exit_tag_performance(pair)
output = "<b>Sell Tag Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (
f"{i+1}.\t <code>{trade['sell_tag']}\t"
f"{i+1}.\t <code>{trade['exit_tag']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit']:.2f}%) "
f"({trade['count']})</code>\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 = "<b>Performance:</b>\n"
output = "<b>Mix Tag Performance:</b>\n"
for i, trade in enumerate(trades):
print(str(trade))
stat_line = (
f"{i+1}.\t <code>{trade['mix_tag']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "

View File

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