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
|
||||
"""
|
||||
BUY_TAG = "buy_tag"
|
||||
EXIT_TAG = "exit_tag"
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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)):
|
||||
"""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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'])
|
||||
|
@ -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]
|
||||
|
@ -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())
|
||||
|
@ -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):
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user