Merge pull request #5710 from theluxaz/freqtrade-development

Added SELL_TAG for trading, backtesting and telegram
This commit is contained in:
Matthias 2021-11-04 07:13:06 +01:00 committed by GitHub
commit 17ecfda2e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 854 additions and 72 deletions

View File

@ -14,3 +14,4 @@ class SignalTagType(Enum):
Enum for signal columns
"""
BUY_TAG = "buy_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) = self.strategy.get_signal(
(buy, sell, buy_tag, _) = self.strategy.get_signal(
pair,
self.strategy.timeframe,
analyzed_df
@ -700,21 +700,22 @@ class FreqtradeBot(LoggingMixin):
logger.debug('Handling %s ...', trade)
(buy, sell) = (False, False)
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, _) = self.strategy.get_signal(
(buy, sell, _, exit_tag) = self.strategy.get_signal(
trade.pair,
self.strategy.timeframe,
analyzed_df
)
logger.debug('checking sell')
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
if self._check_and_execute_exit(trade, exit_rate, buy, sell):
sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
if self._check_and_execute_exit(trade, sell_rate, buy, sell, exit_tag):
return True
logger.debug('Found no sell signal for %s.', trade)
@ -852,18 +853,21 @@ class FreqtradeBot(LoggingMixin):
f"for pair {trade.pair}.")
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
buy: bool, sell: bool) -> bool:
buy: bool, sell: bool, exit_tag: Optional[str]) -> bool:
"""
Check and execute exit
"""
should_sell = self.strategy.should_sell(
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
)
if should_sell.sell_flag:
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
self.execute_trade_exit(trade, exit_rate, should_sell)
logger.info(
f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. '
f'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
@ -1064,7 +1068,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) -> 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
@ -1140,7 +1149,7 @@ class FreqtradeBot(LoggingMixin):
trade.open_order_id = order['id']
trade.sell_order_status = ''
trade.close_rate_requested = limit
trade.sell_reason = sell_reason.sell_reason
trade.sell_reason = exit_tag or sell_reason.sell_reason
# 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)
@ -1181,6 +1190,7 @@ class FreqtradeBot(LoggingMixin):
'current_rate': current_rate,
'profit_amount': profit_trade,
'profit_ratio': profit_ratio,
'buy_tag': trade.buy_tag,
'sell_reason': trade.sell_reason,
'open_date': trade.open_date,
'close_date': trade.close_date or datetime.utcnow(),
@ -1224,6 +1234,7 @@ class FreqtradeBot(LoggingMixin):
'current_rate': current_rate,
'profit_amount': profit_trade,
'profit_ratio': profit_ratio,
'buy_tag': trade.buy_tag,
'sell_reason': trade.sell_reason,
'open_date': trade.open_date,
'close_date': trade.close_date or datetime.now(timezone.utc),

View File

@ -44,6 +44,7 @@ SELL_IDX = 4
LOW_IDX = 5
HIGH_IDX = 6
BUY_TAG_IDX = 7
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']
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'exit_tag']
data: Dict = {}
self.progress.init_step(BacktestState.CONVERT, len(processed))
@ -259,6 +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[:, '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()
@ -270,6 +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[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_tag'].shift(1)
# Update dataprovider cache
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
@ -360,6 +363,16 @@ class Backtesting:
if sell.sell_flag:
trade.close_date = sell_candle_time
trade.sell_reason = sell.sell_reason
# Checks and adds an exit tag, after checking that the length of the
# sell_row has the length for an exit tag column
if(
len(sell_row) > EXIT_TAG_IDX
and sell_row[EXIT_TAG_IDX] is not None
and len(sell_row[EXIT_TAG_IDX]) > 0
):
trade.sell_reason = sell_row[EXIT_TAG_IDX]
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)
@ -387,7 +400,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)

View File

@ -55,6 +55,15 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
'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())
"""
return [first_column, 'Sells', 'Avg Profit %', 'Cum Profit %',
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
'Win Draw Loss Win%']
def _generate_wins_draws_losses(wins, draws, losses):
if wins > 0 and losses == 0:
wl_ratio = '100'
@ -127,6 +136,71 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b
return tabular_data
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 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 = []
if tag_type in results.columns:
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
else:
return []
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]:
"""
Generate small table outlining Backtest results
@ -347,6 +421,10 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
starting_balance=starting_balance,
results=results, skip_nan=False)
buy_tag_results = generate_tag_metrics("buy_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)
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
@ -370,6 +448,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
'best_pair': best_pair,
'worst_pair': worst_pair,
'results_per_pair': pair_results,
'results_per_buy_tag': buy_tag_results,
'sell_reason_summary': sell_reason_stats,
'left_open_trades': left_open_results,
# 'days_breakdown_stats': days_breakdown_stats,
@ -542,6 +621,37 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
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
"""
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'] 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_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
stake_currency: str, period: str) -> str:
"""
@ -687,6 +797,16 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
if(results['results_per_buy_tag'] is not None):
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'],
stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0:
@ -714,6 +834,7 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
if isinstance(table, str) and len(table) > 0:
print('=' * len(table.splitlines()[0]))
print()

View File

@ -853,13 +853,131 @@ class Trade(_DECL_BASE, LocalTrade):
return [
{
'pair': pair,
'profit': profit,
'profit_ratio': profit,
'profit': round(profit * 100, 2), # Compatibility mode
'profit_pct': round(profit * 100, 2),
'profit_abs': profit_abs,
'count': count
}
for pair, profit, profit_abs, count in pair_rates
]
@staticmethod
def get_buy_tag_performance(pair: Optional[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.
"""
filters = [Trade.is_open.is_(False)]
if(pair is not None):
filters.append(Trade.pair == pair)
buy_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(*filters)\
.group_by(Trade.buy_tag) \
.order_by(desc('profit_sum_abs')) \
.all()
return [
{
'buy_tag': buy_tag if buy_tag is not None else "Other",
'profit_ratio': profit,
'profit_pct': round(profit * 100, 2),
'profit_abs': profit_abs,
'count': count
}
for buy_tag, profit, profit_abs, count in buy_tag_perf
]
@staticmethod
def get_sell_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
"""
Returns List of dicts containing all Trades, based on sell reason performance
Can either be average for all pairs or a specific pair provided
NOTE: Not supported in Backtesting.
"""
filters = [Trade.is_open.is_(False)]
if(pair is not None):
filters.append(Trade.pair == pair)
sell_tag_perf = Trade.query.with_entities(
Trade.sell_reason,
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(*filters)\
.group_by(Trade.sell_reason) \
.order_by(desc('profit_sum_abs')) \
.all()
return [
{
'sell_reason': sell_reason if sell_reason is not None else "Other",
'profit_ratio': profit,
'profit_pct': round(profit * 100, 2),
'profit_abs': profit_abs,
'count': count
}
for sell_reason, profit, profit_abs, count in sell_tag_perf
]
@staticmethod
def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
"""
Returns List of dicts containing all Trades, based on buy_tag + sell_reason performance
Can either be average for all pairs or a specific pair provided
NOTE: Not supported in Backtesting.
"""
filters = [Trade.is_open.is_(False)]
if(pair is not None):
filters.append(Trade.pair == pair)
mix_tag_perf = Trade.query.with_entities(
Trade.id,
Trade.buy_tag,
Trade.sell_reason,
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(*filters)\
.group_by(Trade.id) \
.order_by(desc('profit_sum_abs')) \
.all()
return_list: List[Dict] = []
for id, buy_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
buy_tag = buy_tag if buy_tag is not None else "Other"
sell_reason = sell_reason if sell_reason is not None else "Other"
if(sell_reason is not None and buy_tag is not None):
mix_tag = buy_tag + " " + sell_reason
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:
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
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in return_list]
return return_list
@staticmethod
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
"""

View File

@ -63,6 +63,8 @@ class Count(BaseModel):
class PerformanceEntry(BaseModel):
pair: str
profit: float
profit_ratio: float
profit_pct: float
profit_abs: float
count: int

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

View File

@ -107,8 +107,8 @@ class Telegram(RPCHandler):
# this needs refactoring of the whole telegram module (same
# problem in _help()).
valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$',
r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$',
r'/profit$', r'/profit \d+',
r'/trades$', r'/performance$', r'/buys', r'/sells', r'/mix_tags',
r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
r'/stats$', r'/count$', r'/locks$', r'/balance$',
r'/stopbuy$', r'/reload_config$', r'/show_config$',
r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$',
@ -154,6 +154,9 @@ class Telegram(RPCHandler):
CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade),
CommandHandler('performance', self._performance),
CommandHandler('buys', self._buy_tag_performance),
CommandHandler('sells', self._sell_reason_performance),
CommandHandler('mix_tags', self._mix_tag_performance),
CommandHandler('stats', self._stats),
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
@ -175,6 +178,10 @@ class Telegram(RPCHandler):
CallbackQueryHandler(self._profit, pattern='update_profit'),
CallbackQueryHandler(self._balance, pattern='update_balance'),
CallbackQueryHandler(self._performance, pattern='update_performance'),
CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'),
CallbackQueryHandler(self._sell_reason_performance,
pattern='update_sell_reason_performance'),
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
CallbackQueryHandler(self._count, pattern='update_count'),
CallbackQueryHandler(self._forcebuy_inline),
]
@ -238,6 +245,7 @@ class Telegram(RPCHandler):
microsecond=0) - msg['open_date'].replace(microsecond=0)
msg['duration_min'] = msg['duration'].total_seconds() / 60
msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None
msg['emoji'] = self._get_sell_emoji(msg)
# Check if all sell properties are available.
@ -253,6 +261,7 @@ class Telegram(RPCHandler):
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"
@ -852,7 +861,7 @@ class Telegram(RPCHandler):
stat_line = (
f"{i+1}.\t <code>{trade['pair']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit']:.2f}%) "
f"({trade['profit_pct']:.2f}%) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
@ -867,6 +876,111 @@ class Telegram(RPCHandler):
except RPCException as 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 and isinstance(context.args[0], str):
pair = context.args[0]
trades = self._rpc._rpc_buy_tag_performance(pair)
output = "<b>Buy Tag 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_pct']:.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_reason_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 and isinstance(context.args[0], str):
pair = context.args[0]
trades = self._rpc._rpc_sell_reason_performance(pair)
output = "<b>Sell Reason Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (
f"{i+1}.\t <code>{trade['sell_reason']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit_pct']:.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_reason_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 and isinstance(context.args[0], str):
pair = context.args[0]
trades = self._rpc._rpc_mix_tag_performance(pair)
output = "<b>Mix Tag 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
def _count(self, update: Update, context: CallbackContext) -> None:
"""
@ -1043,6 +1157,9 @@ class Telegram(RPCHandler):
" *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an 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_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
"over the last n days`\n"

View File

@ -509,6 +509,7 @@ class IStrategy(ABC, HyperStrategyMixin):
dataframe['buy'] = 0
dataframe['sell'] = 0
dataframe['buy_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)
@ -586,7 +587,7 @@ class IStrategy(ABC, HyperStrategyMixin):
pair: str,
timeframe: str,
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.
Used by Bot to get the signal to buy or sell
@ -597,7 +598,7 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
return False, False, None
return False, False, None, None
latest_date = dataframe['date'].max()
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
@ -612,7 +613,7 @@ class IStrategy(ABC, HyperStrategyMixin):
'Outdated history for pair %s. Last tick is %s minutes old',
pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)
)
return False, False, None
return False, False, None, None
buy = latest[SignalType.BUY.value] == 1
@ -621,6 +622,7 @@ class IStrategy(ABC, HyperStrategyMixin):
sell = latest[SignalType.SELL.value] == 1
buy_tag = latest.get(SignalTagType.BUY_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))
@ -629,8 +631,8 @@ class IStrategy(ABC, HyperStrategyMixin):
current_time=datetime.now(timezone.utc),
timeframe_seconds=timeframe_seconds,
buy=buy):
return False, sell, buy_tag
return buy, sell, buy_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):

View File

@ -186,7 +186,7 @@ def get_patched_worker(mocker, config) -> Worker:
return Worker(args=None, config=config)
def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False, None)) -> None:
def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False, None, None)) -> None:
"""
:param mocker: mocker to patch IStrategy class
:param value: which value IStrategy.get_signal() must return

View File

@ -89,6 +89,7 @@ def mock_trade_2(fee):
open_order_id='dry_run_sell_12345',
strategy='StrategyTestV2',
timeframe=5,
buy_tag='TEST1',
sell_reason='sell_signal',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
@ -241,6 +242,7 @@ def mock_trade_5(fee):
open_rate=0.123,
exchange='binance',
strategy='SampleStrategy',
buy_tag='TEST1',
stoploss_order_id='prod_stoploss_3455',
timeframe=5,
)
@ -295,6 +297,7 @@ def mock_trade_6(fee):
open_rate=0.15,
exchange='binance',
strategy='SampleStrategy',
buy_tag='TEST2',
open_order_id="prod_sell_6",
timeframe=5,
)

View File

@ -54,6 +54,8 @@ def _build_backtest_dataframe(data):
frame[column] = frame[column].astype('float64')
if 'buy_tag' not in columns:
frame['buy_tag'] = None
if 'exit_tag' not in columns:
frame['exit_tag'] = None
# Ensure all candles make kindof sense
assert all(frame['low'] <= frame['close'])

View File

@ -567,6 +567,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
195, # Low
201.5, # High
'', # Buy Signal Name
'', # Exit Signal Name
]
trade = backtesting._enter_trade(pair, row=row)
@ -581,26 +582,27 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
195, # Low
210.5, # High
'', # Buy Signal Name
'', # Exit Signal Name
]
row_detail = pd.DataFrame(
[
[
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc),
1, 200, 199, 0, 197, 200.1, '',
1, 200, 199, 0, 197, 200.1, '', '',
], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=1, tzinfo=timezone.utc),
0, 199, 199.5, 0, 199, 199.7, '',
0, 199, 199.5, 0, 199, 199.7, '', '',
], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=2, tzinfo=timezone.utc),
0, 199.5, 200.5, 0, 199, 200.8, '',
0, 199.5, 200.5, 0, 199, 200.8, '', '',
], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=3, tzinfo=timezone.utc),
0, 200.5, 210.5, 0, 193, 210.5, '', # ROI sell (?)
0, 200.5, 210.5, 0, 193, 210.5, '', '', # ROI sell (?)
], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=4, tzinfo=timezone.utc),
0, 200, 199, 0, 193, 200.1, '',
0, 200, 199, 0, 193, 200.1, '', '',
],
], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"]
], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag", "exit_tag"]
)
# No data available.
@ -614,7 +616,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
assert isinstance(trade, LocalTrade)
# Assign empty ... no result.
backtesting.detail_data[pair] = pd.DataFrame(
[], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"])
[], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag", "exit_tag"])
res = backtesting._get_sell_trade_entry(trade, row)
assert res is None
@ -678,7 +680,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
'min_rate': [0.10370188, 0.10300000000000001],
'max_rate': [0.10501, 0.1038888],
'is_open': [False, False],
'buy_tag': [None, None],
'buy_tag': [None, None]
})
pd.testing.assert_frame_equal(results, expected)
data_pair = processed[pair]

View File

@ -825,8 +825,226 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
assert len(res) == 1
assert res[0]['pair'] == 'ETH/BTC'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit_pct'], 6.2)
def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker,
get_fee=fee,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
res = rpc._rpc_buy_tag_performance(None)
assert len(res) == 1
assert res[0]['buy_tag'] == 'Other'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit_pct'], 6.2)
trade.buy_tag = "TEST_TAG"
res = rpc._rpc_buy_tag_performance(None)
assert len(res) == 1
assert res[0]['buy_tag'] == 'TEST_TAG'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit_pct'], 6.2)
def test_buy_tag_performance_handle_2(mocker, default_conf, markets, fee):
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets)
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
create_mock_trades(fee)
rpc = RPC(freqtradebot)
res = rpc._rpc_buy_tag_performance(None)
assert len(res) == 2
assert res[0]['buy_tag'] == 'TEST1'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit_pct'], 0.5)
assert res[1]['buy_tag'] == 'Other'
assert res[1]['count'] == 1
assert prec_satoshi(res[1]['profit_pct'], 1.0)
# Test for a specific pair
res = rpc._rpc_buy_tag_performance('ETC/BTC')
assert len(res) == 1
assert res[0]['count'] == 1
assert res[0]['buy_tag'] == 'TEST1'
assert prec_satoshi(res[0]['profit_pct'], 0.5)
def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker,
get_fee=fee,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
res = rpc._rpc_sell_reason_performance(None)
assert len(res) == 1
assert res[0]['sell_reason'] == 'Other'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit_pct'], 6.2)
trade.sell_reason = "TEST1"
res = rpc._rpc_sell_reason_performance(None)
assert len(res) == 1
assert res[0]['sell_reason'] == 'TEST1'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit_pct'], 6.2)
def test_sell_reason_performance_handle_2(mocker, default_conf, markets, fee):
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets)
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
create_mock_trades(fee)
rpc = RPC(freqtradebot)
res = rpc._rpc_sell_reason_performance(None)
assert len(res) == 2
assert res[0]['sell_reason'] == 'sell_signal'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit_pct'], 0.5)
assert res[1]['sell_reason'] == 'roi'
assert res[1]['count'] == 1
assert prec_satoshi(res[1]['profit_pct'], 1.0)
# Test for a specific pair
res = rpc._rpc_sell_reason_performance('ETC/BTC')
assert len(res) == 1
assert res[0]['count'] == 1
assert res[0]['sell_reason'] == 'sell_signal'
assert prec_satoshi(res[0]['profit_pct'], 0.5)
def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker,
get_fee=fee,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
res = rpc._rpc_mix_tag_performance(None)
assert len(res) == 1
assert res[0]['mix_tag'] == 'Other Other'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 6.2)
trade.buy_tag = "TESTBUY"
trade.sell_reason = "TESTSELL"
res = rpc._rpc_mix_tag_performance(None)
assert len(res) == 1
assert res[0]['mix_tag'] == 'TESTBUY TESTSELL'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 6.2)
def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets)
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
create_mock_trades(fee)
rpc = RPC(freqtradebot)
res = rpc._rpc_mix_tag_performance(None)
assert len(res) == 2
assert res[0]['mix_tag'] == 'TEST1 sell_signal'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 0.5)
assert res[1]['mix_tag'] == 'Other roi'
assert res[1]['count'] == 1
assert prec_satoshi(res[1]['profit'], 1.0)
# Test for a specific pair
res = rpc._rpc_mix_tag_performance('ETC/BTC')
assert len(res) == 1
assert res[0]['count'] == 1
assert res[0]['mix_tag'] == 'TEST1 sell_signal'
assert prec_satoshi(res[0]['profit'], 0.5)
def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())

View File

@ -812,8 +812,10 @@ def test_api_performance(botclient, fee):
rc = client_get(client, f"{BASE_URI}/performance")
assert_response(rc)
assert len(rc.json()) == 2
assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_abs': 0.01872279},
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}]
assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_pct': 7.61,
'profit_ratio': 0.07609203, 'profit_abs': 0.01872279},
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_pct': -5.57,
'profit_ratio': -0.05570419, 'profit_abs': -0.1150375}]
def test_api_status(botclient, mocker, ticker, fee, markets):

View File

@ -33,6 +33,7 @@ class DummyCls(Telegram):
"""
Dummy class for testing the Telegram @authorized_only decorator
"""
def __init__(self, rpc: RPC, config) -> None:
super().__init__(rpc, config)
self.state = {'called': False}
@ -92,7 +93,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
"['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], "
"['delete'], ['performance'], ['buys'], ['sells'], ['mix_tags'], "
"['stats'], ['daily'], ['count'], ['locks'], "
"['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], "
"['show_config', 'show_conf'], ['stopbuy'], "
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']"
@ -713,6 +715,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
'profit_ratio': 0.0629778,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
'buy_tag': ANY,
'sell_reason': SellType.FORCE_SELL.value,
'open_date': ANY,
'close_date': ANY,
@ -776,6 +779,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
'profit_ratio': -0.05482878,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
'buy_tag': ANY,
'sell_reason': SellType.FORCE_SELL.value,
'open_date': ANY,
'close_date': ANY,
@ -829,6 +833,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
'profit_ratio': -0.00408133,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
'buy_tag': ANY,
'sell_reason': SellType.FORCE_SELL.value,
'open_date': ANY,
'close_date': ANY,
@ -974,6 +979,102 @@ def test_performance_handle(default_conf, update, ticker, fee,
assert '<code>ETH/BTC\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
def test_buy_tag_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
trade.buy_tag = "TESTBUY"
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
telegram._buy_tag_performance(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'Buy Tag Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>TESTBUY\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
def test_sell_reason_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
trade.sell_reason = 'TESTSELL'
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
telegram._sell_reason_performance(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'Sell Reason Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>TESTSELL\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
def test_mix_tag_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
trade.buy_tag = "TESTBUY"
trade.sell_reason = "TESTSELL"
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
telegram._mix_tag_performance(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
assert ('<code>TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)</code>'
in msg_mock.call_args_list[0][0][0])
def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@ -997,9 +1098,9 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
msg = ('<pre> current max total stake\n--------- ----- -------------\n'
' 1 {} {}</pre>').format(
default_conf['max_open_trades'],
default_conf['stake_amount']
)
default_conf['max_open_trades'],
default_conf['stake_amount']
)
assert msg in msg_mock.call_args_list[0][0][0]
@ -1382,6 +1483,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD',
'buy_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(hours=-1),
'close_date': arrow.utcnow(),
@ -1389,6 +1491,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
'*Buy Tag:* `buy_signal1`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `1:00:00 (60.0 min)`\n'
'*Amount:* `1333.33333333`\n'
@ -1412,6 +1515,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'profit_amount': -0.05746268,
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
'buy_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
'close_date': arrow.utcnow(),
@ -1419,6 +1523,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Profit:* `-57.41%`\n'
'*Buy Tag:* `buy_signal1`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
'*Amount:* `1333.33333333`\n'
@ -1483,6 +1588,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None:
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD',
'buy_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(hours=-1),
'close_date': arrow.utcnow(),
@ -1574,12 +1680,14 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD',
'buy_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Profit:* `-57.41%`\n'
'*Buy Tag:* `buy_signal1`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `2:35:03 (155.1 min)`\n'
'*Amount:* `1333.33333333`\n'

View File

@ -30,7 +30,7 @@ _STRATEGY = StrategyTestV2(config={})
_STRATEGY.dp = DataProvider({}, None, None)
def test_returns_latest_signal(mocker, default_conf, ohlcv_history):
def test_returns_latest_signal(ohlcv_history):
ohlcv_history.loc[1, 'date'] = arrow.utcnow()
# Take a copy to correctly modify the call
mocked_history = ohlcv_history.copy()
@ -38,20 +38,39 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history):
mocked_history['buy'] = 0
mocked_history.loc[1, 'sell'] = 1
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None)
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None, None)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 1
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None)
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None, None)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 0
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None)
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None, None)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 1
mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01'
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, 'buy_signal_01')
assert _STRATEGY.get_signal(
'ETH/BTC',
'5m',
mocked_history) == (
True,
False,
'buy_signal_01',
None)
mocked_history.loc[1, 'buy_tag'] = None
mocked_history.loc[1, 'exit_tag'] = 'sell_signal_01'
assert _STRATEGY.get_signal(
'ETH/BTC',
'5m',
mocked_history) == (
True,
False,
None,
'sell_signal_01')
def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history):
@ -68,17 +87,24 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history):
def test_get_signal_empty(default_conf, mocker, caplog):
assert (False, False, None) == _STRATEGY.get_signal(
assert (False, False, None, None) == _STRATEGY.get_signal(
'foo', default_conf['timeframe'], DataFrame()
)
assert log_has('Empty candle (OHLCV) data for pair foo', caplog)
caplog.clear()
assert (False, False, None) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None)
assert (
False,
False,
None,
None) == _STRATEGY.get_signal(
'bar',
default_conf['timeframe'],
None)
assert log_has('Empty candle (OHLCV) data for pair bar', caplog)
caplog.clear()
assert (False, False, None) == _STRATEGY.get_signal(
assert (False, False, None, None) == _STRATEGY.get_signal(
'baz',
default_conf['timeframe'],
DataFrame([])
@ -118,7 +144,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY, 'assert_df')
assert (False, False, None) == _STRATEGY.get_signal(
assert (False, False, None, None) == _STRATEGY.get_signal(
'xyz',
default_conf['timeframe'],
mocked_history
@ -140,7 +166,7 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY, 'assert_df')
assert (True, False, None) == _STRATEGY.get_signal(
assert (True, False, None, None) == _STRATEGY.get_signal(
'xyz',
default_conf['timeframe'],
mocked_history
@ -653,7 +679,7 @@ def test_strategy_safe_wrapper(value):
ret = strategy_safe_wrapper(working_method, message='DeadBeef')(value)
assert type(ret) == type(value)
assert isinstance(ret, type(value))
assert ret == value

View File

@ -236,7 +236,7 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker,
# stoploss shoud be hit
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
if not ignore_strat_sl:
assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog)
assert log_has_re(r'Executing Sell for NEO/BTC. Reason: stop_loss.*', caplog)
assert trade.sell_reason == SellType.STOP_LOSS.value
@ -450,7 +450,7 @@ def test_create_trade_no_signal(default_conf_usdt, fee, mocker) -> None:
)
default_conf_usdt['stake_amount'] = 10
freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, value=(False, False, None))
patch_get_signal(freqtrade, value=(False, False, None, None))
Trade.query = MagicMock()
Trade.query.filter = MagicMock()
@ -677,7 +677,7 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")])
mocker.patch(
'freqtrade.strategy.interface.IStrategy.get_signal',
return_value=(False, False, '')
return_value=(False, False, '', '')
)
mocker.patch('time.sleep', return_value=None)
@ -1808,7 +1808,7 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_
assert trade.is_open is True
freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(False, True, None))
patch_get_signal(freqtrade, value=(False, True, None, 'sell_signal1'))
assert freqtrade.handle_trade(trade) is True
assert trade.open_order_id == limit_sell_order_usdt['id']
@ -1819,6 +1819,7 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_
assert trade.close_profit == 0.09451372
assert trade.calc_profit() == 5.685
assert trade.close_date is not None
assert trade.sell_reason == 'sell_signal1'
def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open,
@ -1836,7 +1837,7 @@ def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_or
)
freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, value=(True, True, None))
patch_get_signal(freqtrade, value=(True, True, None, None))
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
freqtrade.enter_positions()
@ -1855,7 +1856,7 @@ def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_or
assert trades[0].is_open is True
# Buy and Sell are not triggering, so doing nothing ...
patch_get_signal(freqtrade, value=(False, False, None))
patch_get_signal(freqtrade, value=(False, False, None, None))
assert freqtrade.handle_trade(trades[0]) is False
trades = Trade.query.all()
nb_trades = len(trades)
@ -1863,7 +1864,7 @@ def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_or
assert trades[0].is_open is True
# Buy and Sell are triggering, so doing nothing ...
patch_get_signal(freqtrade, value=(True, True, None))
patch_get_signal(freqtrade, value=(True, True, None, None))
assert freqtrade.handle_trade(trades[0]) is False
trades = Trade.query.all()
nb_trades = len(trades)
@ -1871,7 +1872,7 @@ def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_or
assert trades[0].is_open is True
# Sell is triggering, guess what : we are Selling!
patch_get_signal(freqtrade, value=(False, True, None))
patch_get_signal(freqtrade, value=(False, True, None, None))
trades = Trade.query.all()
assert freqtrade.handle_trade(trades[0]) is True
@ -1905,7 +1906,7 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_o
# we might just want to check if we are in a sell condition without
# executing
# if ROI is reached we must sell
patch_get_signal(freqtrade, value=(False, True, None))
patch_get_signal(freqtrade, value=(False, True, None, None))
assert freqtrade.handle_trade(trade)
assert log_has("ETH/USDT - Required profit reached. sell_type=SellType.ROI",
caplog)
@ -1934,10 +1935,10 @@ def test_handle_trade_use_sell_signal(default_conf_usdt, ticker_usdt, limit_buy_
trade = Trade.query.first()
trade.is_open = True
patch_get_signal(freqtrade, value=(False, False, None))
patch_get_signal(freqtrade, value=(False, False, None, None))
assert not freqtrade.handle_trade(trade)
patch_get_signal(freqtrade, value=(False, True, None))
patch_get_signal(freqtrade, value=(False, True, None, None))
assert freqtrade.handle_trade(trade)
assert log_has("ETH/USDT - Sell signal received. sell_type=SellType.SELL_SIGNAL",
caplog)
@ -2579,6 +2580,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
'limit': 2.2,
'amount': 30.0,
'order_type': 'limit',
'buy_tag': None,
'open_rate': 2.0,
'current_rate': 2.3,
'profit_amount': 5.685,
@ -2632,6 +2634,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
'limit': 2.01,
'amount': 30.0,
'order_type': 'limit',
'buy_tag': None,
'open_rate': 2.0,
'current_rate': 2.0,
'profit_amount': -0.00075,
@ -2699,6 +2702,7 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe
'limit': 2.25,
'amount': 30.0,
'order_type': 'limit',
'buy_tag': None,
'open_rate': 2.0,
'current_rate': 2.3,
'profit_amount': 7.18125,
@ -2758,6 +2762,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
'limit': 1.98,
'amount': 30.0,
'order_type': 'limit',
'buy_tag': None,
'open_rate': 2.0,
'current_rate': 2.0,
'profit_amount': -0.8985,
@ -2975,6 +2980,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee,
'limit': 2.2,
'amount': 30.0,
'order_type': 'market',
'buy_tag': None,
'open_rate': 2.0,
'current_rate': 2.3,
'profit_amount': 5.685,
@ -3068,7 +3074,7 @@ def test_sell_profit_only(
trade = Trade.query.first()
trade.update(limit_buy_order_usdt)
freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(False, True, None))
patch_get_signal(freqtrade, value=(False, True, None, None))
assert freqtrade.handle_trade(trade) is handle_first
if handle_second:
@ -3103,7 +3109,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_
trade = Trade.query.first()
amnt = trade.amount
trade.update(limit_buy_order_usdt)
patch_get_signal(freqtrade, value=(False, True, None))
patch_get_signal(freqtrade, value=(False, True, None, None))
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985))
assert freqtrade.handle_trade(trade) is True
@ -3212,11 +3218,11 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt,
trade = Trade.query.first()
trade.update(limit_buy_order_usdt)
freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(True, True, None))
patch_get_signal(freqtrade, value=(True, True, None, None))
assert freqtrade.handle_trade(trade) is False
# Test if buy-signal is absent (should sell due to roi = true)
patch_get_signal(freqtrade, value=(False, True, None))
patch_get_signal(freqtrade, value=(False, True, None, None))
assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.ROI.value
@ -3402,11 +3408,11 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd
trade = Trade.query.first()
trade.update(limit_buy_order_usdt)
# Sell due to min_roi_reached
patch_get_signal(freqtrade, value=(True, True, None))
patch_get_signal(freqtrade, value=(True, True, None, None))
assert freqtrade.handle_trade(trade) is True
# Test if buy-signal is absent
patch_get_signal(freqtrade, value=(False, True, None))
patch_get_signal(freqtrade, value=(False, True, None, None))
assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.ROI.value
@ -3848,7 +3854,7 @@ def test_order_book_ask_strategy(
freqtrade.wallets.update()
assert trade.is_open is True
patch_get_signal(freqtrade, value=(False, True, None))
patch_get_signal(freqtrade, value=(False, True, None, None))
assert freqtrade.handle_trade(trade) is True
assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0]

View File

@ -1317,6 +1317,10 @@ def test_Trade_object_idem():
'get_open_trades_without_assigned_fees',
'get_open_order_trades',
'get_trades',
'get_sell_reason_performance',
'get_buy_tag_performance',
'get_mix_tag_performance',
)
# Parent (LocalTrade) should have the same attributes