Merge pull request #5710 from theluxaz/freqtrade-development
Added SELL_TAG for trading, backtesting and telegram
This commit is contained in:
commit
17ecfda2e8
@ -14,3 +14,4 @@ class SignalTagType(Enum):
|
|||||||
Enum for signal columns
|
Enum for signal columns
|
||||||
"""
|
"""
|
||||||
BUY_TAG = "buy_tag"
|
BUY_TAG = "buy_tag"
|
||||||
|
EXIT_TAG = "exit_tag"
|
||||||
|
@ -201,11 +201,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if len(open_trades) != 0:
|
if len(open_trades) != 0:
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.WARNING,
|
'type': RPCMessageType.WARNING,
|
||||||
'status': f"{len(open_trades)} open trades active.\n\n"
|
'status': f"{len(open_trades)} open trades active.\n\n"
|
||||||
f"Handle these trades manually on {self.exchange.name}, "
|
f"Handle these trades manually on {self.exchange.name}, "
|
||||||
f"or '/start' the bot again and use '/stopbuy' "
|
f"or '/start' the bot again and use '/stopbuy' "
|
||||||
f"to handle open trades gracefully. \n"
|
f"to handle open trades gracefully. \n"
|
||||||
f"{'Trades are simulated.' if self.config['dry_run'] else ''}",
|
f"{'Trades are simulated.' if self.config['dry_run'] else ''}",
|
||||||
}
|
}
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
@ -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, _) = self.strategy.get_signal(
|
||||||
pair,
|
pair,
|
||||||
self.strategy.timeframe,
|
self.strategy.timeframe,
|
||||||
analyzed_df
|
analyzed_df
|
||||||
@ -700,21 +700,22 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
|
|
||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
|
exit_tag = None
|
||||||
|
|
||||||
if (self.config.get('use_sell_signal', True) or
|
if (self.config.get('use_sell_signal', True) or
|
||||||
self.config.get('ignore_roi_if_buy_signal', False)):
|
self.config.get('ignore_roi_if_buy_signal', False)):
|
||||||
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, _, exit_tag) = self.strategy.get_signal(
|
||||||
trade.pair,
|
trade.pair,
|
||||||
self.strategy.timeframe,
|
self.strategy.timeframe,
|
||||||
analyzed_df
|
analyzed_df
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug('checking sell')
|
logger.debug('checking sell')
|
||||||
exit_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_exit(trade, exit_rate, buy, sell):
|
if self._check_and_execute_exit(trade, sell_rate, buy, sell, exit_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 +853,21 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
f"for pair {trade.pair}.")
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
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
|
Check and execute exit
|
||||||
"""
|
"""
|
||||||
|
|
||||||
should_sell = self.strategy.should_sell(
|
should_sell = self.strategy.should_sell(
|
||||||
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
|
trade, exit_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(
|
||||||
self.execute_trade_exit(trade, exit_rate, should_sell)
|
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 True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1064,7 +1068,12 @@ 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,
|
||||||
|
exit_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
|
||||||
@ -1140,7 +1149,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.open_order_id = order['id']
|
trade.open_order_id = order['id']
|
||||||
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 = exit_tag or sell_reason.sell_reason
|
||||||
# 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)
|
||||||
@ -1181,6 +1190,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_trade,
|
'profit_amount': profit_trade,
|
||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
|
'buy_tag': trade.buy_tag,
|
||||||
'sell_reason': trade.sell_reason,
|
'sell_reason': trade.sell_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.utcnow(),
|
'close_date': trade.close_date or datetime.utcnow(),
|
||||||
@ -1224,6 +1234,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_trade,
|
'profit_amount': profit_trade,
|
||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
|
'buy_tag': trade.buy_tag,
|
||||||
'sell_reason': trade.sell_reason,
|
'sell_reason': trade.sell_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||||
|
@ -44,6 +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
|
||||||
|
EXIT_TAG_IDX = 8
|
||||||
|
|
||||||
|
|
||||||
class Backtesting:
|
class Backtesting:
|
||||||
@ -247,7 +248,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', 'exit_tag']
|
||||||
data: Dict = {}
|
data: Dict = {}
|
||||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
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[:, '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[:, 'exit_tag'] = None # cleanup if exit_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()
|
||||||
@ -270,6 +272,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[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_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)
|
||||||
@ -360,6 +363,16 @@ class Backtesting:
|
|||||||
if sell.sell_flag:
|
if sell.sell_flag:
|
||||||
trade.close_date = sell_candle_time
|
trade.close_date = sell_candle_time
|
||||||
trade.sell_reason = sell.sell_reason
|
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)
|
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)
|
||||||
|
|
||||||
@ -387,7 +400,7 @@ class Backtesting:
|
|||||||
detail_data = detail_data.loc[
|
detail_data = detail_data.loc[
|
||||||
(detail_data['date'] >= sell_candle_time) &
|
(detail_data['date'] >= sell_candle_time) &
|
||||||
(detail_data['date'] < sell_candle_end)
|
(detail_data['date'] < sell_candle_end)
|
||||||
].copy()
|
].copy()
|
||||||
if len(detail_data) == 0:
|
if len(detail_data) == 0:
|
||||||
# Fall back to "regular" data if no detail data was found for this candle
|
# 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)
|
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||||
|
@ -55,6 +55,15 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
|
|||||||
'Win Draw Loss Win%']
|
'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):
|
def _generate_wins_draws_losses(wins, draws, losses):
|
||||||
if wins > 0 and losses == 0:
|
if wins > 0 and losses == 0:
|
||||||
wl_ratio = '100'
|
wl_ratio = '100'
|
||||||
@ -127,6 +136,71 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b
|
|||||||
return tabular_data
|
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]:
|
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Generate small table outlining Backtest results
|
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,
|
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", 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,
|
||||||
@ -370,6 +448,7 @@ 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,
|
||||||
'sell_reason_summary': sell_reason_stats,
|
'sell_reason_summary': sell_reason_stats,
|
||||||
'left_open_trades': left_open_results,
|
'left_open_trades': left_open_results,
|
||||||
# 'days_breakdown_stats': days_breakdown_stats,
|
# '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")
|
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]],
|
def text_table_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
|
||||||
stake_currency: str, period: str) -> str:
|
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(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
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'],
|
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:
|
||||||
@ -714,6 +834,7 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
|||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -853,13 +853,131 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'profit': profit,
|
'profit_ratio': profit,
|
||||||
|
'profit': round(profit * 100, 2), # Compatibility mode
|
||||||
|
'profit_pct': round(profit * 100, 2),
|
||||||
'profit_abs': profit_abs,
|
'profit_abs': profit_abs,
|
||||||
'count': count
|
'count': count
|
||||||
}
|
}
|
||||||
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: 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
|
@staticmethod
|
||||||
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
|
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
|
||||||
"""
|
"""
|
||||||
|
@ -63,6 +63,8 @@ class Count(BaseModel):
|
|||||||
class PerformanceEntry(BaseModel):
|
class PerformanceEntry(BaseModel):
|
||||||
pair: str
|
pair: str
|
||||||
profit: float
|
profit: float
|
||||||
|
profit_ratio: float
|
||||||
|
profit_pct: float
|
||||||
profit_abs: float
|
profit_abs: float
|
||||||
count: int
|
count: int
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ class RPC:
|
|||||||
val = {
|
val = {
|
||||||
'dry_run': config['dry_run'],
|
'dry_run': config['dry_run'],
|
||||||
'stake_currency': config['stake_currency'],
|
'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'],
|
'stake_amount': config['stake_amount'],
|
||||||
'available_capital': config.get('available_capital'),
|
'available_capital': config.get('available_capital'),
|
||||||
'max_open_trades': (config['max_open_trades']
|
'max_open_trades': (config['max_open_trades']
|
||||||
@ -682,10 +682,36 @@ class RPC:
|
|||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
"""
|
"""
|
||||||
pair_rates = Trade.get_overall_performance()
|
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
|
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]:
|
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:
|
||||||
|
@ -107,8 +107,8 @@ class Telegram(RPCHandler):
|
|||||||
# this needs refactoring of the whole telegram module (same
|
# this needs refactoring of the whole telegram module (same
|
||||||
# problem in _help()).
|
# problem in _help()).
|
||||||
valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$',
|
valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$',
|
||||||
r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$',
|
r'/trades$', r'/performance$', r'/buys', r'/sells', r'/mix_tags',
|
||||||
r'/profit$', r'/profit \d+',
|
r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
|
||||||
r'/stats$', r'/count$', r'/locks$', r'/balance$',
|
r'/stats$', r'/count$', r'/locks$', r'/balance$',
|
||||||
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$',
|
||||||
@ -154,6 +154,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_reason_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),
|
||||||
@ -175,6 +178,10 @@ 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._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._count, pattern='update_count'),
|
||||||
CallbackQueryHandler(self._forcebuy_inline),
|
CallbackQueryHandler(self._forcebuy_inline),
|
||||||
]
|
]
|
||||||
@ -238,6 +245,7 @@ class Telegram(RPCHandler):
|
|||||||
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
||||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
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)
|
msg['emoji'] = self._get_sell_emoji(msg)
|
||||||
|
|
||||||
# Check if all sell properties are available.
|
# Check if all sell properties are available.
|
||||||
@ -253,6 +261,7 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
|
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
|
||||||
"*Profit:* `{profit_percent:.2f}%{profit_extra}`\n"
|
"*Profit:* `{profit_percent:.2f}%{profit_extra}`\n"
|
||||||
|
"*Buy Tag:* `{buy_tag}`\n"
|
||||||
"*Sell Reason:* `{sell_reason}`\n"
|
"*Sell Reason:* `{sell_reason}`\n"
|
||||||
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
|
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
|
||||||
"*Amount:* `{amount:.8f}`\n"
|
"*Amount:* `{amount:.8f}`\n"
|
||||||
@ -852,7 +861,7 @@ class Telegram(RPCHandler):
|
|||||||
stat_line = (
|
stat_line = (
|
||||||
f"{i+1}.\t <code>{trade['pair']}\t"
|
f"{i+1}.\t <code>{trade['pair']}\t"
|
||||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
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")
|
f"({trade['count']})</code>\n")
|
||||||
|
|
||||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||||
@ -867,6 +876,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 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
|
@authorized_only
|
||||||
def _count(self, update: Update, context: CallbackContext) -> None:
|
def _count(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1043,6 +1157,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_tags <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"
|
||||||
|
@ -509,6 +509,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['exit_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)
|
||||||
@ -586,7 +587,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
|
||||||
@ -597,7 +598,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
||||||
logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
|
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_date = dataframe['date'].max()
|
||||||
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
|
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',
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)
|
pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)
|
||||||
)
|
)
|
||||||
return False, False, None
|
return False, False, None, None
|
||||||
|
|
||||||
buy = latest[SignalType.BUY.value] == 1
|
buy = latest[SignalType.BUY.value] == 1
|
||||||
|
|
||||||
@ -621,6 +622,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)
|
||||||
|
exit_tag = latest.get(SignalTagType.EXIT_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))
|
||||||
@ -629,8 +631,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, exit_tag
|
||||||
return buy, sell, buy_tag
|
return buy, sell, buy_tag, exit_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):
|
||||||
|
@ -186,7 +186,7 @@ def get_patched_worker(mocker, config) -> Worker:
|
|||||||
return Worker(args=None, config=config)
|
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 mocker: mocker to patch IStrategy class
|
||||||
:param value: which value IStrategy.get_signal() must return
|
:param value: which value IStrategy.get_signal() must return
|
||||||
|
@ -89,6 +89,7 @@ def mock_trade_2(fee):
|
|||||||
open_order_id='dry_run_sell_12345',
|
open_order_id='dry_run_sell_12345',
|
||||||
strategy='StrategyTestV2',
|
strategy='StrategyTestV2',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
buy_tag='TEST1',
|
||||||
sell_reason='sell_signal',
|
sell_reason='sell_signal',
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||||
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
||||||
@ -241,6 +242,7 @@ def mock_trade_5(fee):
|
|||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
|
buy_tag='TEST1',
|
||||||
stoploss_order_id='prod_stoploss_3455',
|
stoploss_order_id='prod_stoploss_3455',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
)
|
)
|
||||||
@ -295,6 +297,7 @@ def mock_trade_6(fee):
|
|||||||
open_rate=0.15,
|
open_rate=0.15,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
|
buy_tag='TEST2',
|
||||||
open_order_id="prod_sell_6",
|
open_order_id="prod_sell_6",
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
)
|
)
|
||||||
|
@ -54,6 +54,8 @@ def _build_backtest_dataframe(data):
|
|||||||
frame[column] = frame[column].astype('float64')
|
frame[column] = frame[column].astype('float64')
|
||||||
if 'buy_tag' not in columns:
|
if 'buy_tag' not in columns:
|
||||||
frame['buy_tag'] = None
|
frame['buy_tag'] = None
|
||||||
|
if 'exit_tag' not in columns:
|
||||||
|
frame['exit_tag'] = None
|
||||||
|
|
||||||
# Ensure all candles make kindof sense
|
# Ensure all candles make kindof sense
|
||||||
assert all(frame['low'] <= frame['close'])
|
assert all(frame['low'] <= frame['close'])
|
||||||
|
@ -567,6 +567,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
195, # Low
|
195, # Low
|
||||||
201.5, # High
|
201.5, # High
|
||||||
'', # Buy Signal Name
|
'', # Buy Signal Name
|
||||||
|
'', # Exit Signal Name
|
||||||
]
|
]
|
||||||
|
|
||||||
trade = backtesting._enter_trade(pair, row=row)
|
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
|
195, # Low
|
||||||
210.5, # High
|
210.5, # High
|
||||||
'', # Buy Signal Name
|
'', # Buy Signal Name
|
||||||
|
'', # Exit Signal Name
|
||||||
]
|
]
|
||||||
row_detail = pd.DataFrame(
|
row_detail = pd.DataFrame(
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc),
|
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),
|
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),
|
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),
|
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),
|
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.
|
# No data available.
|
||||||
@ -614,7 +616,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
assert isinstance(trade, LocalTrade)
|
assert isinstance(trade, LocalTrade)
|
||||||
# Assign empty ... no result.
|
# Assign empty ... no result.
|
||||||
backtesting.detail_data[pair] = pd.DataFrame(
|
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)
|
res = backtesting._get_sell_trade_entry(trade, row)
|
||||||
assert res is None
|
assert res is None
|
||||||
@ -678,7 +680,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
'min_rate': [0.10370188, 0.10300000000000001],
|
'min_rate': [0.10370188, 0.10300000000000001],
|
||||||
'max_rate': [0.10501, 0.1038888],
|
'max_rate': [0.10501, 0.1038888],
|
||||||
'is_open': [False, False],
|
'is_open': [False, False],
|
||||||
'buy_tag': [None, None],
|
'buy_tag': [None, None]
|
||||||
})
|
})
|
||||||
pd.testing.assert_frame_equal(results, expected)
|
pd.testing.assert_frame_equal(results, expected)
|
||||||
data_pair = processed[pair]
|
data_pair = processed[pair]
|
||||||
|
@ -825,8 +825,226 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
|||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
assert res[0]['pair'] == 'ETH/BTC'
|
assert res[0]['pair'] == 'ETH/BTC'
|
||||||
assert res[0]['count'] == 1
|
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)
|
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:
|
def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
@ -812,8 +812,10 @@ def test_api_performance(botclient, fee):
|
|||||||
rc = client_get(client, f"{BASE_URI}/performance")
|
rc = client_get(client, f"{BASE_URI}/performance")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert len(rc.json()) == 2
|
assert len(rc.json()) == 2
|
||||||
assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_abs': 0.01872279},
|
assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_pct': 7.61,
|
||||||
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}]
|
'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):
|
def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||||
|
@ -33,6 +33,7 @@ class DummyCls(Telegram):
|
|||||||
"""
|
"""
|
||||||
Dummy class for testing the Telegram @authorized_only decorator
|
Dummy class for testing the Telegram @authorized_only decorator
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rpc: RPC, config) -> None:
|
def __init__(self, rpc: RPC, config) -> None:
|
||||||
super().__init__(rpc, config)
|
super().__init__(rpc, config)
|
||||||
self.state = {'called': False}
|
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'], "
|
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
|
"['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'], "
|
"['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], "
|
||||||
"['show_config', 'show_conf'], ['stopbuy'], "
|
"['show_config', 'show_conf'], ['stopbuy'], "
|
||||||
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']"
|
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']"
|
||||||
@ -713,6 +715,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
'profit_ratio': 0.0629778,
|
'profit_ratio': 0.0629778,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'buy_tag': ANY,
|
||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
@ -776,6 +779,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
'profit_ratio': -0.05482878,
|
'profit_ratio': -0.05482878,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'buy_tag': ANY,
|
||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_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,
|
'profit_ratio': -0.00408133,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'buy_tag': ANY,
|
||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_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]
|
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:
|
def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'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'
|
msg = ('<pre> current max total stake\n--------- ----- -------------\n'
|
||||||
' 1 {} {}</pre>').format(
|
' 1 {} {}</pre>').format(
|
||||||
default_conf['max_open_trades'],
|
default_conf['max_open_trades'],
|
||||||
default_conf['stake_amount']
|
default_conf['stake_amount']
|
||||||
)
|
)
|
||||||
assert msg in msg_mock.call_args_list[0][0][0]
|
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,
|
'profit_ratio': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'buy_tag': 'buy_signal1',
|
||||||
'sell_reason': SellType.STOP_LOSS.value,
|
'sell_reason': SellType.STOP_LOSS.value,
|
||||||
'open_date': arrow.utcnow().shift(hours=-1),
|
'open_date': arrow.utcnow().shift(hours=-1),
|
||||||
'close_date': arrow.utcnow(),
|
'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] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
||||||
'*Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
'*Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
||||||
|
'*Buy Tag:* `buy_signal1`\n'
|
||||||
'*Sell Reason:* `stop_loss`\n'
|
'*Sell Reason:* `stop_loss`\n'
|
||||||
'*Duration:* `1:00:00 (60.0 min)`\n'
|
'*Duration:* `1:00:00 (60.0 min)`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
@ -1412,6 +1515,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
'profit_amount': -0.05746268,
|
'profit_amount': -0.05746268,
|
||||||
'profit_ratio': -0.57405275,
|
'profit_ratio': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
|
'buy_tag': 'buy_signal1',
|
||||||
'sell_reason': SellType.STOP_LOSS.value,
|
'sell_reason': SellType.STOP_LOSS.value,
|
||||||
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
||||||
'close_date': arrow.utcnow(),
|
'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] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
||||||
'*Profit:* `-57.41%`\n'
|
'*Profit:* `-57.41%`\n'
|
||||||
|
'*Buy Tag:* `buy_signal1`\n'
|
||||||
'*Sell Reason:* `stop_loss`\n'
|
'*Sell Reason:* `stop_loss`\n'
|
||||||
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
|
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
@ -1483,6 +1588,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None:
|
|||||||
'profit_ratio': -0.57405275,
|
'profit_ratio': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'buy_tag': 'buy_signal1',
|
||||||
'sell_reason': SellType.STOP_LOSS.value,
|
'sell_reason': SellType.STOP_LOSS.value,
|
||||||
'open_date': arrow.utcnow().shift(hours=-1),
|
'open_date': arrow.utcnow().shift(hours=-1),
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
@ -1574,12 +1680,14 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
'profit_ratio': -0.57405275,
|
'profit_ratio': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'buy_tag': 'buy_signal1',
|
||||||
'sell_reason': SellType.STOP_LOSS.value,
|
'sell_reason': SellType.STOP_LOSS.value,
|
||||||
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
|
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
|
||||||
'*Profit:* `-57.41%`\n'
|
'*Profit:* `-57.41%`\n'
|
||||||
|
'*Buy Tag:* `buy_signal1`\n'
|
||||||
'*Sell Reason:* `stop_loss`\n'
|
'*Sell Reason:* `stop_loss`\n'
|
||||||
'*Duration:* `2:35:03 (155.1 min)`\n'
|
'*Duration:* `2:35:03 (155.1 min)`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
|
@ -30,7 +30,7 @@ _STRATEGY = StrategyTestV2(config={})
|
|||||||
_STRATEGY.dp = DataProvider({}, None, None)
|
_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()
|
ohlcv_history.loc[1, 'date'] = arrow.utcnow()
|
||||||
# Take a copy to correctly modify the call
|
# Take a copy to correctly modify the call
|
||||||
mocked_history = ohlcv_history.copy()
|
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['buy'] = 0
|
||||||
mocked_history.loc[1, 'sell'] = 1
|
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, 'sell'] = 0
|
||||||
mocked_history.loc[1, 'buy'] = 1
|
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, 'sell'] = 0
|
||||||
mocked_history.loc[1, 'buy'] = 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, 'sell'] = 0
|
||||||
mocked_history.loc[1, 'buy'] = 1
|
mocked_history.loc[1, 'buy'] = 1
|
||||||
mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01'
|
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):
|
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):
|
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()
|
'foo', default_conf['timeframe'], DataFrame()
|
||||||
)
|
)
|
||||||
assert log_has('Empty candle (OHLCV) data for pair foo', caplog)
|
assert log_has('Empty candle (OHLCV) data for pair foo', caplog)
|
||||||
caplog.clear()
|
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)
|
assert log_has('Empty candle (OHLCV) data for pair bar', caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
assert (False, False, None) == _STRATEGY.get_signal(
|
assert (False, False, None, None) == _STRATEGY.get_signal(
|
||||||
'baz',
|
'baz',
|
||||||
default_conf['timeframe'],
|
default_conf['timeframe'],
|
||||||
DataFrame([])
|
DataFrame([])
|
||||||
@ -118,7 +144,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
|
|||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch.object(_STRATEGY, 'assert_df')
|
mocker.patch.object(_STRATEGY, 'assert_df')
|
||||||
|
|
||||||
assert (False, False, None) == _STRATEGY.get_signal(
|
assert (False, False, None, None) == _STRATEGY.get_signal(
|
||||||
'xyz',
|
'xyz',
|
||||||
default_conf['timeframe'],
|
default_conf['timeframe'],
|
||||||
mocked_history
|
mocked_history
|
||||||
@ -140,7 +166,7 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history):
|
|||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch.object(_STRATEGY, 'assert_df')
|
mocker.patch.object(_STRATEGY, 'assert_df')
|
||||||
|
|
||||||
assert (True, False, None) == _STRATEGY.get_signal(
|
assert (True, False, None, None) == _STRATEGY.get_signal(
|
||||||
'xyz',
|
'xyz',
|
||||||
default_conf['timeframe'],
|
default_conf['timeframe'],
|
||||||
mocked_history
|
mocked_history
|
||||||
@ -653,7 +679,7 @@ def test_strategy_safe_wrapper(value):
|
|||||||
|
|
||||||
ret = strategy_safe_wrapper(working_method, message='DeadBeef')(value)
|
ret = strategy_safe_wrapper(working_method, message='DeadBeef')(value)
|
||||||
|
|
||||||
assert type(ret) == type(value)
|
assert isinstance(ret, type(value))
|
||||||
assert ret == value
|
assert ret == value
|
||||||
|
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker,
|
|||||||
# stoploss shoud be hit
|
# stoploss shoud be hit
|
||||||
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
|
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
|
||||||
if 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
|
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
|
default_conf_usdt['stake_amount'] = 10
|
||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
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 = MagicMock()
|
||||||
Trade.query.filter = 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")])
|
inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")])
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.strategy.interface.IStrategy.get_signal',
|
'freqtrade.strategy.interface.IStrategy.get_signal',
|
||||||
return_value=(False, False, '')
|
return_value=(False, False, '', '')
|
||||||
)
|
)
|
||||||
mocker.patch('time.sleep', return_value=None)
|
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
|
assert trade.is_open is True
|
||||||
freqtrade.wallets.update()
|
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 freqtrade.handle_trade(trade) is True
|
||||||
assert trade.open_order_id == limit_sell_order_usdt['id']
|
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.close_profit == 0.09451372
|
||||||
assert trade.calc_profit() == 5.685
|
assert trade.calc_profit() == 5.685
|
||||||
assert trade.close_date is not None
|
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,
|
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)
|
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.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
|
||||||
freqtrade.enter_positions()
|
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
|
assert trades[0].is_open is True
|
||||||
|
|
||||||
# Buy and Sell are not triggering, so doing nothing ...
|
# 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
|
assert freqtrade.handle_trade(trades[0]) is False
|
||||||
trades = Trade.query.all()
|
trades = Trade.query.all()
|
||||||
nb_trades = len(trades)
|
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
|
assert trades[0].is_open is True
|
||||||
|
|
||||||
# Buy and Sell are triggering, so doing nothing ...
|
# 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
|
assert freqtrade.handle_trade(trades[0]) is False
|
||||||
trades = Trade.query.all()
|
trades = Trade.query.all()
|
||||||
nb_trades = len(trades)
|
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
|
assert trades[0].is_open is True
|
||||||
|
|
||||||
# Sell is triggering, guess what : we are Selling!
|
# 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()
|
trades = Trade.query.all()
|
||||||
assert freqtrade.handle_trade(trades[0]) is True
|
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
|
# we might just want to check if we are in a sell condition without
|
||||||
# executing
|
# executing
|
||||||
# if ROI is reached we must sell
|
# 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 freqtrade.handle_trade(trade)
|
||||||
assert log_has("ETH/USDT - Required profit reached. sell_type=SellType.ROI",
|
assert log_has("ETH/USDT - Required profit reached. sell_type=SellType.ROI",
|
||||||
caplog)
|
caplog)
|
||||||
@ -1934,10 +1935,10 @@ def test_handle_trade_use_sell_signal(default_conf_usdt, ticker_usdt, limit_buy_
|
|||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
trade.is_open = True
|
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)
|
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 freqtrade.handle_trade(trade)
|
||||||
assert log_has("ETH/USDT - Sell signal received. sell_type=SellType.SELL_SIGNAL",
|
assert log_has("ETH/USDT - Sell signal received. sell_type=SellType.SELL_SIGNAL",
|
||||||
caplog)
|
caplog)
|
||||||
@ -2579,6 +2580,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
|
|||||||
'limit': 2.2,
|
'limit': 2.2,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
|
'buy_tag': None,
|
||||||
'open_rate': 2.0,
|
'open_rate': 2.0,
|
||||||
'current_rate': 2.3,
|
'current_rate': 2.3,
|
||||||
'profit_amount': 5.685,
|
'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,
|
'limit': 2.01,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
|
'buy_tag': None,
|
||||||
'open_rate': 2.0,
|
'open_rate': 2.0,
|
||||||
'current_rate': 2.0,
|
'current_rate': 2.0,
|
||||||
'profit_amount': -0.00075,
|
'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,
|
'limit': 2.25,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
|
'buy_tag': None,
|
||||||
'open_rate': 2.0,
|
'open_rate': 2.0,
|
||||||
'current_rate': 2.3,
|
'current_rate': 2.3,
|
||||||
'profit_amount': 7.18125,
|
'profit_amount': 7.18125,
|
||||||
@ -2758,6 +2762,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
|
|||||||
'limit': 1.98,
|
'limit': 1.98,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
|
'buy_tag': None,
|
||||||
'open_rate': 2.0,
|
'open_rate': 2.0,
|
||||||
'current_rate': 2.0,
|
'current_rate': 2.0,
|
||||||
'profit_amount': -0.8985,
|
'profit_amount': -0.8985,
|
||||||
@ -2975,6 +2980,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee,
|
|||||||
'limit': 2.2,
|
'limit': 2.2,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
'order_type': 'market',
|
'order_type': 'market',
|
||||||
|
'buy_tag': None,
|
||||||
'open_rate': 2.0,
|
'open_rate': 2.0,
|
||||||
'current_rate': 2.3,
|
'current_rate': 2.3,
|
||||||
'profit_amount': 5.685,
|
'profit_amount': 5.685,
|
||||||
@ -3068,7 +3074,7 @@ def test_sell_profit_only(
|
|||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
trade.update(limit_buy_order_usdt)
|
trade.update(limit_buy_order_usdt)
|
||||||
freqtrade.wallets.update()
|
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
|
assert freqtrade.handle_trade(trade) is handle_first
|
||||||
|
|
||||||
if handle_second:
|
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()
|
trade = Trade.query.first()
|
||||||
amnt = trade.amount
|
amnt = trade.amount
|
||||||
trade.update(limit_buy_order_usdt)
|
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))
|
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985))
|
||||||
|
|
||||||
assert freqtrade.handle_trade(trade) is True
|
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 = Trade.query.first()
|
||||||
trade.update(limit_buy_order_usdt)
|
trade.update(limit_buy_order_usdt)
|
||||||
freqtrade.wallets.update()
|
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
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
|
||||||
# Test if buy-signal is absent (should sell due to roi = true)
|
# 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 freqtrade.handle_trade(trade) is True
|
||||||
assert trade.sell_reason == SellType.ROI.value
|
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 = Trade.query.first()
|
||||||
trade.update(limit_buy_order_usdt)
|
trade.update(limit_buy_order_usdt)
|
||||||
# Sell due to min_roi_reached
|
# 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
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
|
||||||
# Test if buy-signal is absent
|
# 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 freqtrade.handle_trade(trade) is True
|
||||||
assert trade.sell_reason == SellType.ROI.value
|
assert trade.sell_reason == SellType.ROI.value
|
||||||
|
|
||||||
@ -3848,7 +3854,7 @@ def test_order_book_ask_strategy(
|
|||||||
freqtrade.wallets.update()
|
freqtrade.wallets.update()
|
||||||
assert trade.is_open is True
|
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 freqtrade.handle_trade(trade) is True
|
||||||
assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0]
|
assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0]
|
||||||
|
|
||||||
|
@ -1317,6 +1317,10 @@ def test_Trade_object_idem():
|
|||||||
'get_open_trades_without_assigned_fees',
|
'get_open_trades_without_assigned_fees',
|
||||||
'get_open_order_trades',
|
'get_open_order_trades',
|
||||||
'get_trades',
|
'get_trades',
|
||||||
|
'get_sell_reason_performance',
|
||||||
|
'get_buy_tag_performance',
|
||||||
|
'get_mix_tag_performance',
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parent (LocalTrade) should have the same attributes
|
# Parent (LocalTrade) should have the same attributes
|
||||||
|
Loading…
Reference in New Issue
Block a user