Merge branch 'develop' into pr/mkavinkumar1/6545

This commit is contained in:
Matthias 2022-06-20 20:04:35 +02:00
commit 0bfb0febe9
22 changed files with 235 additions and 62 deletions

View File

@ -66,12 +66,12 @@ jobs:
- name: Tests - name: Tests
run: | run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc pytest --random-order --cov=freqtrade --cov-config=.coveragerc
if: matrix.python-version != '3.9' if: matrix.python-version != '3.9' || matrix.os != 'ubuntu-22.04'
- name: Tests incl. ccxt compatibility tests - name: Tests incl. ccxt compatibility tests
run: | run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
if: matrix.python-version == '3.9' if: matrix.python-version == '3.9' && matrix.os == 'ubuntu-22.04'
- name: Coveralls - name: Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.9') if: (runner.os == 'Linux' && matrix.python-version == '3.9')

View File

@ -13,7 +13,7 @@ repos:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
additional_dependencies: additional_dependencies:
- types-cachetools==5.0.1 - types-cachetools==5.0.2
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.27.30 - types-requests==2.27.30
- types-tabulate==0.8.9 - types-tabulate==0.8.9

View File

@ -300,6 +300,7 @@ A backtesting result will look like that:
| Absolute profit | 0.00762792 BTC | | Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% | | Total profit % | 76.2% |
| CAGR % | 460.87% | | CAGR % | 460.87% |
| Profit factor | 1.11 |
| Avg. stake amount | 0.001 BTC | | Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC | | Total trade volume | 0.429 BTC |
| | | | | |
@ -399,6 +400,7 @@ It contains some useful key metrics about performance of your strategy on backte
| Absolute profit | 0.00762792 BTC | | Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% | | Total profit % | 76.2% |
| CAGR % | 460.87% | | CAGR % | 460.87% |
| Profit factor | 1.11 |
| Avg. stake amount | 0.001 BTC | | Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC | | Total trade volume | 0.429 BTC |
| | | | | |
@ -444,6 +446,8 @@ It contains some useful key metrics about performance of your strategy on backte
- `Final balance`: Final balance - starting balance + absolute profit. - `Final balance`: Final balance - starting balance + absolute profit.
- `Absolute profit`: Profit made in stake currency. - `Absolute profit`: Profit made in stake currency.
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital Starting capital) / Starting capital`. - `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital Starting capital) / Starting capital`.
- `CAGR %`: Compound annual growth rate.
- `Profit factor`: profit / loss.
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.

View File

@ -1,5 +1,5 @@
mkdocs==1.3.0 mkdocs==1.3.0
mkdocs-material==8.3.4 mkdocs-material==8.3.6
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.5 pymdown-extensions==9.5
jinja2==3.1.2 jinja2==3.1.2

View File

@ -171,8 +171,8 @@ official commands. You can ask at any moment for help with `/help`.
| `/locks` | Show currently locked pairs. | `/locks` | Show currently locked pairs.
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id). | `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
| `/forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`). | `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `/forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `/fx` | alias for `/forceexit` | `/fx` | alias for `/forceexit`
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True) | `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True) | `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
@ -270,10 +270,15 @@ Return a summary of your profit/loss and performance.
> **Latest Trade opened:** `2 minutes ago` > **Latest Trade opened:** `2 minutes ago`
> **Avg. Duration:** `2:33:45` > **Avg. Duration:** `2:33:45`
> **Best Performing:** `PAY/BTC: 50.23%` > **Best Performing:** `PAY/BTC: 50.23%`
> **Trading volume:** `0.5 BTC`
> **Profit factor:** `1.04`
> **Max Drawdown:** `9.23% (0.01255 BTC)`
The relative profit of `1.2%` is the average profit per trade. The relative profit of `1.2%` is the average profit per trade.
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`. The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
### /forceexit <trade_id> ### /forceexit <trade_id>
@ -281,6 +286,7 @@ Starting capital is either taken from the `available_capital` setting, or calcul
!!! Tip !!! Tip
You can get a list of all open trades by calling `/forceexit` without parameter, which will show a list of buttons to simply exit a trade. You can get a list of all open trades by calling `/forceexit` without parameter, which will show a list of buttons to simply exit a trade.
This command has an alias in `/fx` - which has the same capabilities, but is faster to type in "emergency" situations.
### /forcelong <pair> [rate] | /forceshort <pair> [rate] ### /forcelong <pair> [rate] | /forceshort <pair> [rate]

View File

@ -3,6 +3,7 @@ import logging
from datetime import datetime from datetime import datetime
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -24,6 +25,8 @@ class Gateio(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
"ohlcv_volume_currency": "quote", "ohlcv_volume_currency": "quote",
"time_in_force_parameter": "timeInForce",
"order_time_in_force": ['gtc', 'ioc'],
"stoploss_order_types": {"limit": "limit"}, "stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
} }
@ -40,13 +43,33 @@ class Gateio(Exchange):
] ]
def validate_ordertypes(self, order_types: Dict) -> None: def validate_ordertypes(self, order_types: Dict) -> None:
super().validate_ordertypes(order_types)
if self.trading_mode != TradingMode.FUTURES: if self.trading_mode != TradingMode.FUTURES:
if any(v == 'market' for k, v in order_types.items()): if any(v == 'market' for k, v in order_types.items()):
raise OperationalException( raise OperationalException(
f'Exchange {self.name} does not support market orders.') f'Exchange {self.name} does not support market orders.')
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'gtc',
) -> Dict:
params = super()._get_params(
side=side,
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
params['type'] = 'market'
param = self._ft_has.get('time_in_force_parameter', '')
params.update({param: 'ioc'})
return params
def get_trades_for_order(self, order_id: str, pair: str, since: datetime, def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
params: Optional[Dict] = None) -> List: params: Optional[Dict] = None) -> List:
trades = super().get_trades_for_order(order_id, pair, since, params) trades = super().get_trades_for_order(order_id, pair, since, params)
@ -61,7 +84,8 @@ class Gateio(Exchange):
pair_fees = self._trading_fees.get(pair, {}) pair_fees = self._trading_fees.get(pair, {})
if pair_fees: if pair_fees:
for idx, trade in enumerate(trades): for idx, trade in enumerate(trades):
if trade.get('fee', {}).get('cost') is None: fee = trade.get('fee', {})
if fee and fee.get('cost') is None:
takerOrMaker = trade.get('takerOrMaker', 'taker') takerOrMaker = trade.get('takerOrMaker', 'taker')
if pair_fees.get(takerOrMaker) is not None: if pair_fees.get(takerOrMaker) is not None:
trades[idx]['fee'] = { trades[idx]['fee'] = {

View File

@ -1083,6 +1083,7 @@ class Backtesting:
# Close trade # Close trade
open_trade_count -= 1 open_trade_count -= 1
open_trades[pair].remove(t) open_trades[pair].remove(t)
LocalTrade.trades_open.remove(t)
self.wallets.update() self.wallets.update()
# 2. Process entries. # 2. Process entries.
@ -1106,6 +1107,8 @@ class Backtesting:
open_trade_count += 1 open_trade_count += 1
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
open_trades[pair].append(trade) open_trades[pair].append(trade)
LocalTrade.add_bt_trade(trade)
self.wallets.update()
for trade in list(open_trades[pair]): for trade in list(open_trades[pair]):
# 3. Process entry orders. # 3. Process entry orders.
@ -1113,7 +1116,6 @@ class Backtesting:
if order and self._get_order_filled(order.price, row): if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade) order.close_bt_order(current_time, trade)
trade.open_order_id = None trade.open_order_id = None
LocalTrade.add_bt_trade(trade)
self.wallets.update() self.wallets.update()
# 4. Create exit orders (if any) # 4. Create exit orders (if any)

View File

@ -416,6 +416,9 @@ def generate_strategy_stats(pairlist: List[str],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
winning_profit = results.loc[results['profit_abs'] > 0, 'profit_abs'].sum()
losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum()
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
backtest_days = (max_date - min_date).days or 1 backtest_days = (max_date - min_date).days or 1
strat_stats = { strat_stats = {
@ -443,6 +446,7 @@ def generate_strategy_stats(pairlist: List[str],
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(), 'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(), 'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']), 'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
'profit_factor': profit_factor,
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT), 'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
'backtest_start_ts': int(min_date.timestamp() * 1000), 'backtest_start_ts': int(min_date.timestamp() * 1000),
'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT), 'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
@ -497,8 +501,10 @@ def generate_strategy_stats(pairlist: List[str],
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val, (drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
max_drawdown) = calculate_max_drawdown( max_drawdown) = calculate_max_drawdown(
results, value_col='profit_abs', starting_balance=start_balance) results, value_col='profit_abs', starting_balance=start_balance)
# max_relative_drawdown = Underwater
(_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown( (_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
results, value_col='profit_abs', starting_balance=start_balance, relative=True) results, value_col='profit_abs', starting_balance=start_balance, relative=True)
strat_stats.update({ strat_stats.update({
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use 'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
'max_drawdown_account': max_drawdown, 'max_drawdown_account': max_drawdown,
@ -777,6 +783,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])), strat_results['stake_currency'])),
('Total profit %', f"{strat_results['profit_total']:.2%}"), ('Total profit %', f"{strat_results['profit_total']:.2%}"),
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'), ('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
in strat_results else 'N/A'),
('Trades per day', strat_results['trades_per_day']), ('Trades per day', strat_results['trades_per_day']),
('Avg. daily profit %', ('Avg. daily profit %',
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),

View File

@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String, from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
UniqueConstraint, desc, func) UniqueConstraint, desc, func)
from sqlalchemy.orm import Query, relationship from sqlalchemy.orm import Query, lazyload, relationship
from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES,
BuySell, LongShort) BuySell, LongShort)
@ -1177,7 +1177,7 @@ class Trade(_DECL_BASE, LocalTrade):
) )
@staticmethod @staticmethod
def get_trades(trade_filter=None) -> Query: def get_trades(trade_filter=None, include_orders: bool = True) -> Query:
""" """
Helper function to query Trades using filters. Helper function to query Trades using filters.
NOTE: Not supported in Backtesting. NOTE: Not supported in Backtesting.
@ -1192,9 +1192,14 @@ class Trade(_DECL_BASE, LocalTrade):
if trade_filter is not None: if trade_filter is not None:
if not isinstance(trade_filter, list): if not isinstance(trade_filter, list):
trade_filter = [trade_filter] trade_filter = [trade_filter]
return Trade.query.filter(*trade_filter) this_query = Trade.query.filter(*trade_filter)
else: else:
return Trade.query this_query = Trade.query
if not include_orders:
# Don't load order relations
# Consider using noload or raiseload instead of lazyload
this_query = this_query.options(lazyload(Trade.orders))
return this_query
@staticmethod @staticmethod
def get_open_order_trades() -> List['Trade']: def get_open_order_trades() -> List['Trade']:
@ -1414,3 +1419,18 @@ class Trade(_DECL_BASE, LocalTrade):
.group_by(Trade.pair) \ .group_by(Trade.pair) \
.order_by(desc('profit_sum')).first() .order_by(desc('profit_sum')).first()
return best_pair return best_pair
@staticmethod
def get_trading_volume(start_date: datetime = datetime.fromtimestamp(0)) -> float:
"""
Get Trade volume based on Orders
NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum)
"""
trading_volume = Order.query.with_entities(
func.sum(Order.cost).label('volume')
).filter(
Order.order_filled_date >= start_date,
Order.status == 'closed'
).scalar()
return trading_volume

View File

@ -104,6 +104,10 @@ class Profit(BaseModel):
best_pair_profit_ratio: float best_pair_profit_ratio: float
winning_trades: int winning_trades: int
losing_trades: int losing_trades: int
profit_factor: float
max_drawdown: float
max_drawdown_abs: float
trading_volume: Optional[float]
class SellReason(BaseModel): class SellReason(BaseModel):
@ -279,6 +283,7 @@ class OpenTradeSchema(TradeSchema):
class TradeResponse(BaseModel): class TradeResponse(BaseModel):
trades: List[TradeSchema] trades: List[TradeSchema]
trades_count: int trades_count: int
offset: int
total_trades: int total_trades: int

View File

@ -18,6 +18,7 @@ from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State, from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State,
TradingMode) TradingMode)
from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exceptions import ExchangeError, PricingError
@ -364,6 +365,7 @@ class RPC:
return { return {
"trades": output, "trades": output,
"trades_count": len(output), "trades_count": len(output),
"offset": offset,
"total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(), "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
} }
@ -378,7 +380,7 @@ class RPC:
return 'losses' return 'losses'
else: else:
return 'draws' return 'draws'
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)]) trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
# Sell reason # Sell reason
exit_reasons = {} exit_reasons = {}
for trade in trades: for trade in trades:
@ -406,7 +408,8 @@ class RPC:
""" Returns cumulative profit statistics """ """ Returns cumulative profit statistics """
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
Trade.is_open.is_(True)) Trade.is_open.is_(True))
trades: List[Trade] = Trade.get_trades(trade_filter).order_by(Trade.id).all() trades: List[Trade] = Trade.get_trades(
trade_filter, include_orders=False).order_by(Trade.id).all()
profit_all_coin = [] profit_all_coin = []
profit_all_ratio = [] profit_all_ratio = []
@ -415,6 +418,8 @@ class RPC:
durations = [] durations = []
winning_trades = 0 winning_trades = 0
losing_trades = 0 losing_trades = 0
winning_profit = 0.0
losing_profit = 0.0
for trade in trades: for trade in trades:
current_rate: float = 0.0 current_rate: float = 0.0
@ -430,8 +435,10 @@ class RPC:
profit_closed_ratio.append(profit_ratio) profit_closed_ratio.append(profit_ratio)
if trade.close_profit >= 0: if trade.close_profit >= 0:
winning_trades += 1 winning_trades += 1
winning_profit += trade.close_profit_abs
else: else:
losing_trades += 1 losing_trades += 1
losing_profit += trade.close_profit_abs
else: else:
# Get current rate # Get current rate
try: try:
@ -447,6 +454,7 @@ class RPC:
profit_all_ratio.append(profit_ratio) profit_all_ratio.append(profit_ratio)
best_pair = Trade.get_best_pair(start_date) best_pair = Trade.get_best_pair(start_date)
trading_volume = Trade.get_trading_volume(start_date)
# Prepare data to display # Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8) profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
@ -470,6 +478,21 @@ class RPC:
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf')
trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
'profit_abs': trade.close_profit_abs}
for trade in trades if not trade.is_open])
max_drawdown_abs = 0.0
max_drawdown = 0.0
if len(trades_df) > 0:
try:
(max_drawdown_abs, _, _, _, _, max_drawdown) = calculate_max_drawdown(
trades_df, value_col='profit_abs', starting_balance=starting_balance)
except ValueError:
# ValueError if no losing trade.
pass
profit_all_fiat = self._fiat_converter.convert_amount( profit_all_fiat = self._fiat_converter.convert_amount(
profit_all_coin_sum, profit_all_coin_sum,
stake_currency, stake_currency,
@ -508,6 +531,10 @@ class RPC:
'best_pair_profit_ratio': best_pair[1] if best_pair else 0, 'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
'winning_trades': winning_trades, 'winning_trades': winning_trades,
'losing_trades': losing_trades, 'losing_trades': losing_trades,
'profit_factor': profit_factor,
'max_drawdown': max_drawdown,
'max_drawdown_abs': max_drawdown_abs,
'trading_volume': trading_volume,
} }
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:

View File

@ -235,6 +235,14 @@ class Telegram(RPCHandler):
# This can take up to `timeout` from the call to `start_polling`. # This can take up to `timeout` from the call to `start_polling`.
self._updater.stop() self._updater.stop()
def _exchange_from_msg(self, msg: Dict[str, Any]) -> str:
"""
Extracts the exchange name from the given message.
:param msg: The message to extract the exchange name from.
:return: The exchange name.
"""
return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}"
def _format_entry_msg(self, msg: Dict[str, Any]) -> str: def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
if self._rpc._fiat_converter: if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
@ -247,7 +255,7 @@ class Telegram(RPCHandler):
entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long' entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
else {'enter': 'Short', 'entered': 'Shorted'}) else {'enter': 'Short', 'entered': 'Shorted'})
message = ( message = (
f"{emoji} *{msg['exchange']}:*" f"{emoji} *{self._exchange_from_msg(msg)}:*"
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}" f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
f" (#{msg['trade_id']})\n" f" (#{msg['trade_id']})\n"
) )
@ -313,7 +321,7 @@ class Telegram(RPCHandler):
else: else:
cp_extra = '' cp_extra = ''
message = ( message = (
f"{msg['emoji']} *{msg['exchange']}:* " f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* " f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
@ -357,33 +365,33 @@ class Telegram(RPCHandler):
elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL): elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit' msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
message = ("\N{WARNING SIGN} *{exchange}:* " message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
"Cancelling {message_side} Order for {pair} (#{trade_id}). " f"Cancelling {msg['message_side']} Order for {msg['pair']} "
"Reason: {reason}.".format(**msg)) f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
elif msg_type == RPCMessageType.PROTECTION_TRIGGER: elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
message = ( message = (
"*Protection* triggered due to {reason}. " f"*Protection* triggered due to {msg['reason']}. "
"`{pair}` will be locked until `{lock_end_time}`." f"`{msg['pair']}` will be locked until `{msg['lock_end_time']}`."
).format(**msg) )
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL: elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
message = ( message = (
"*Protection* triggered due to {reason}. " f"*Protection* triggered due to {msg['reason']}. "
"*All pairs* will be locked until `{lock_end_time}`." f"*All pairs* will be locked until `{msg['lock_end_time']}`."
).format(**msg) )
elif msg_type == RPCMessageType.STATUS: elif msg_type == RPCMessageType.STATUS:
message = '*Status:* `{status}`'.format(**msg) message = f"*Status:* `{msg['status']}`"
elif msg_type == RPCMessageType.WARNING: elif msg_type == RPCMessageType.WARNING:
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`"
elif msg_type == RPCMessageType.STARTUP: elif msg_type == RPCMessageType.STARTUP:
message = '{status}'.format(**msg) message = f"{msg['status']}"
else: else:
raise NotImplementedError('Unknown message type: {}'.format(msg_type)) raise NotImplementedError(f"Unknown message type: {msg_type}")
return message return message
def send_msg(self, msg: Dict[str, Any]) -> None: def send_msg(self, msg: Dict[str, Any]) -> None:
@ -767,12 +775,18 @@ class Telegram(RPCHandler):
f"*Total Trade Count:* `{trade_count}`\n" f"*Total Trade Count:* `{trade_count}`\n"
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* " f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
f"`{first_trade_date}`\n" f"`{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}\n`" f"*Latest Trade opened:* `{latest_trade_date}`\n"
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
) )
if stats['closed_trade_count'] > 0: if stats['closed_trade_count'] > 0:
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" markdown_msg += (
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`") f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`\n"
f"*Trading volume:* `{round_coin_value(stats['trading_volume'], stake_cur)}`\n"
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`"
)
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
query=update.callback_query) query=update.callback_query)
@ -912,7 +926,7 @@ class Telegram(RPCHandler):
:return: None :return: None
""" """
msg = self._rpc._rpc_start() msg = self._rpc._rpc_start()
self._send_msg('Status: `{status}`'.format(**msg)) self._send_msg(f"Status: `{msg['status']}`")
@authorized_only @authorized_only
def _stop(self, update: Update, context: CallbackContext) -> None: def _stop(self, update: Update, context: CallbackContext) -> None:
@ -924,7 +938,7 @@ class Telegram(RPCHandler):
:return: None :return: None
""" """
msg = self._rpc._rpc_stop() msg = self._rpc._rpc_stop()
self._send_msg('Status: `{status}`'.format(**msg)) self._send_msg(f"Status: `{msg['status']}`")
@authorized_only @authorized_only
def _reload_config(self, update: Update, context: CallbackContext) -> None: def _reload_config(self, update: Update, context: CallbackContext) -> None:
@ -936,7 +950,7 @@ class Telegram(RPCHandler):
:return: None :return: None
""" """
msg = self._rpc._rpc_reload_config() msg = self._rpc._rpc_reload_config()
self._send_msg('Status: `{status}`'.format(**msg)) self._send_msg(f"Status: `{msg['status']}`")
@authorized_only @authorized_only
def _stopbuy(self, update: Update, context: CallbackContext) -> None: def _stopbuy(self, update: Update, context: CallbackContext) -> None:
@ -948,7 +962,7 @@ class Telegram(RPCHandler):
:return: None :return: None
""" """
msg = self._rpc._rpc_stopbuy() msg = self._rpc._rpc_stopbuy()
self._send_msg('Status: `{status}`'.format(**msg)) self._send_msg(f"Status: `{msg['status']}`")
@authorized_only @authorized_only
def _force_exit(self, update: Update, context: CallbackContext) -> None: def _force_exit(self, update: Update, context: CallbackContext) -> None:
@ -1110,9 +1124,9 @@ class Telegram(RPCHandler):
trade_id = int(context.args[0]) trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id) msg = self._rpc._rpc_delete(trade_id)
self._send_msg(( self._send_msg((
'`{result_msg}`\n' f"`{msg['result_msg']}`\n"
'Please make sure to take care of this asset on the exchange manually.' 'Please make sure to take care of this asset on the exchange manually.'
).format(**msg)) ))
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
@ -1440,7 +1454,7 @@ class Telegram(RPCHandler):
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, " "*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
"regardless of profit`\n" "regardless of profit`\n"
"*/fe <trade_id>|all:* `Alias to /forceexit`\n" "*/fx <trade_id>|all:* `Alias to /forceexit`\n"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}" f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n" "*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/whitelist:* `Show current whitelist` \n" "*/whitelist:* `Show current whitelist` \n"

View File

@ -22,7 +22,7 @@ time-machine==2.7.0
nbconvert==6.5.0 nbconvert==6.5.0
# mypy types # mypy types
types-cachetools==5.0.1 types-cachetools==5.0.2
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.27.30 types-requests==2.27.30
types-tabulate==0.8.9 types-tabulate==0.8.9

View File

@ -2,7 +2,7 @@ numpy==1.22.4
pandas==1.4.2 pandas==1.4.2
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.87.12 ccxt==1.88.15
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==37.0.2 cryptography==37.0.2
aiohttp==3.8.1 aiohttp==3.8.1
@ -41,7 +41,7 @@ aiofiles==0.8.0
psutil==5.9.1 psutil==5.9.1
# Support for colorized terminal output # Support for colorized terminal output
colorama==0.4.4 colorama==0.4.5
# Building config files interactively # Building config files interactively
questionary==1.10.0 questionary==1.10.0
prompt-toolkit==3.0.29 prompt-toolkit==3.0.29

View File

@ -29,6 +29,7 @@ def mock_order_1(is_short: bool):
'average': 0.123, 'average': 0.123,
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'cost': 15.129,
'remaining': 0.0, 'remaining': 0.0,
} }
@ -65,6 +66,7 @@ def mock_order_2(is_short: bool):
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'cost': 15.129,
'remaining': 0.0, 'remaining': 0.0,
} }
@ -79,6 +81,7 @@ def mock_order_2_sell(is_short: bool):
'price': 0.128, 'price': 0.128,
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'cost': 15.129,
'remaining': 0.0, 'remaining': 0.0,
} }
@ -126,6 +129,7 @@ def mock_order_3(is_short: bool):
'price': 0.05, 'price': 0.05,
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'cost': 15.129,
'remaining': 0.0, 'remaining': 0.0,
} }
@ -141,6 +145,7 @@ def mock_order_3_sell(is_short: bool):
'average': 0.06, 'average': 0.06,
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'cost': 15.129,
'remaining': 0.0, 'remaining': 0.0,
} }
@ -186,6 +191,7 @@ def mock_order_4(is_short: bool):
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
'filled': 0.0, 'filled': 0.0,
'cost': 15.129,
'remaining': 123.0, 'remaining': 123.0,
} }
@ -225,6 +231,7 @@ def mock_order_5(is_short: bool):
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'cost': 15.129,
'remaining': 0.0, 'remaining': 0.0,
} }
@ -239,6 +246,7 @@ def mock_order_5_stoploss(is_short: bool):
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
'filled': 0.0, 'filled': 0.0,
'cost': 0.0,
'remaining': 123.0, 'remaining': 123.0,
} }
@ -281,6 +289,7 @@ def mock_order_6(is_short: bool):
'price': 0.15, 'price': 0.15,
'amount': 2.0, 'amount': 2.0,
'filled': 2.0, 'filled': 2.0,
'cost': 0.3,
'remaining': 0.0, 'remaining': 0.0,
} }
@ -295,6 +304,7 @@ def mock_order_6_sell(is_short: bool):
'price': 0.15 if is_short else 0.20, 'price': 0.15 if is_short else 0.20,
'amount': 2.0, 'amount': 2.0,
'filled': 0.0, 'filled': 0.0,
'cost': 0.0,
'remaining': 2.0, 'remaining': 2.0,
} }
@ -337,6 +347,7 @@ def short_order():
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'cost': 15.129,
'remaining': 0.0, 'remaining': 0.0,
} }
@ -351,6 +362,7 @@ def exit_short_order():
'price': 0.128, 'price': 0.128,
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'cost': 15.744,
'remaining': 0.0, 'remaining': 0.0,
} }
@ -424,6 +436,7 @@ def leverage_order():
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'remaining': 0.0, 'remaining': 0.0,
'cost': 15.129,
'leverage': 5.0 'leverage': 5.0
} }
@ -439,6 +452,7 @@ def leverage_order_sell():
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'remaining': 0.0, 'remaining': 0.0,
'cost': 15.744,
'leverage': 5.0 'leverage': 5.0
} }

View File

@ -199,8 +199,13 @@ class TestCCXTExchange():
l2 = exchange.fetch_l2_order_book(pair) l2 = exchange.fetch_l2_order_book(pair)
assert 'asks' in l2 assert 'asks' in l2
assert 'bids' in l2 assert 'bids' in l2
assert len(l2['asks']) >= 1
assert len(l2['bids']) >= 1
l2_limit_range = exchange._ft_has['l2_limit_range'] l2_limit_range = exchange._ft_has['l2_limit_range']
l2_limit_range_required = exchange._ft_has['l2_limit_range_required'] l2_limit_range_required = exchange._ft_has['l2_limit_range_required']
if exchangename == 'gateio':
# TODO: Gateio is unstable here at the moment, ignoring the limit partially.
return
for val in [1, 2, 5, 25, 100]: for val in [1, 2, 5, 25, 100]:
l2 = exchange.fetch_l2_order_book(pair, val) l2 = exchange.fetch_l2_order_book(pair, val)
if not l2_limit_range or val in l2_limit_range: if not l2_limit_range or val in l2_limit_range:

View File

@ -33,6 +33,12 @@ def test_validate_order_types_gateio(default_conf, mocker):
match=r'Exchange .* does not support market orders.'): match=r'Exchange .* does not support market orders.'):
ExchangeResolver.load_exchange('gateio', default_conf, True) ExchangeResolver.load_exchange('gateio', default_conf, True)
# market-orders supported on futures markets.
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
ex = ExchangeResolver.load_exchange('gateio', default_conf, True)
assert ex
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_fetch_stoploss_order_gateio(default_conf, mocker): def test_fetch_stoploss_order_gateio(default_conf, mocker):

View File

@ -7,6 +7,7 @@ import pytest
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
from freqtrade.enums import ExitType from freqtrade.enums import ExitType
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence.trade_model import LocalTrade
from tests.conftest import patch_exchange from tests.conftest import patch_exchange
from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
_get_frame_time_from_offset, tests_timeframe) _get_frame_time_from_offset, tests_timeframe)
@ -964,5 +965,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
assert res.close_date == _get_frame_time_from_offset(trade.close_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
assert res.is_short == trade.is_short assert res.is_short == trade.is_short
assert len(LocalTrade.trades) == len(data.trades)
assert len(LocalTrade.trades_open) == 0
backtesting.cleanup() backtesting.cleanup()
del backtesting del backtesting

View File

@ -578,9 +578,10 @@ def test_api_trades(botclient, mocker, fee, markets, is_short):
) )
rc = client_get(client, f"{BASE_URI}/trades") rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc) assert_response(rc)
assert len(rc.json()) == 3 assert len(rc.json()) == 4
assert rc.json()['trades_count'] == 0 assert rc.json()['trades_count'] == 0
assert rc.json()['total_trades'] == 0 assert rc.json()['total_trades'] == 0
assert rc.json()['offset'] == 0
create_mock_trades(fee, is_short=is_short) create_mock_trades(fee, is_short=is_short)
Trade.query.session.flush() Trade.query.session.flush()
@ -724,7 +725,9 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
'profit_closed_fiat': -83.19455985, 'profit_closed_ratio_mean': -0.0075, 'profit_closed_fiat': -83.19455985, 'profit_closed_ratio_mean': -0.0075,
'profit_closed_percent_mean': -0.75, 'profit_closed_ratio_sum': -0.015, 'profit_closed_percent_mean': -0.75, 'profit_closed_ratio_sum': -0.015,
'profit_closed_percent_sum': -1.5, 'profit_closed_ratio': -6.739057628404269e-06, 'profit_closed_percent_sum': -1.5, 'profit_closed_ratio': -6.739057628404269e-06,
'profit_closed_percent': -0.0, 'winning_trades': 0, 'losing_trades': 2} 'profit_closed_percent': -0.0, 'winning_trades': 0, 'losing_trades': 2,
'profit_factor': 0.0, 'trading_volume': 91.074,
}
), ),
( (
False, False,
@ -737,7 +740,9 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
'profit_closed_fiat': 9.124559849999999, 'profit_closed_ratio_mean': 0.0075, 'profit_closed_fiat': 9.124559849999999, 'profit_closed_ratio_mean': 0.0075,
'profit_closed_percent_mean': 0.75, 'profit_closed_ratio_sum': 0.015, 'profit_closed_percent_mean': 0.75, 'profit_closed_ratio_sum': 0.015,
'profit_closed_percent_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07, 'profit_closed_percent_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07,
'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0} 'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0,
'profit_factor': None, 'trading_volume': 91.074,
}
), ),
( (
None, None,
@ -750,7 +755,9 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
'profit_closed_fiat': -67.02260985, 'profit_closed_ratio_mean': 0.0025, 'profit_closed_fiat': -67.02260985, 'profit_closed_ratio_mean': 0.0025,
'profit_closed_percent_mean': 0.25, 'profit_closed_ratio_sum': 0.005, 'profit_closed_percent_mean': 0.25, 'profit_closed_ratio_sum': 0.005,
'profit_closed_percent_sum': 0.5, 'profit_closed_ratio': -5.429078808526421e-06, 'profit_closed_percent_sum': 0.5, 'profit_closed_ratio': -5.429078808526421e-06,
'profit_closed_percent': -0.0, 'winning_trades': 1, 'losing_trades': 1} 'profit_closed_percent': -0.0, 'winning_trades': 1, 'losing_trades': 1,
'profit_factor': 0.02775724835771106, 'trading_volume': 91.074,
}
) )
]) ])
def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected): def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected):
@ -803,6 +810,10 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
'closed_trade_count': 2, 'closed_trade_count': 2,
'winning_trades': expected['winning_trades'], 'winning_trades': expected['winning_trades'],
'losing_trades': expected['losing_trades'], 'losing_trades': expected['losing_trades'],
'profit_factor': expected['profit_factor'],
'max_drawdown': ANY,
'max_drawdown_abs': ANY,
'trading_volume': expected['trading_volume'],
} }

View File

@ -704,11 +704,13 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
assert '∙ `6.253 USD`' in msg_mock.call_args_list[-1][0][0] assert '∙ `6.253 USD`' in msg_mock.call_args_list[-1][0][0]
assert '*Best Performing:* `ETH/USDT: 9.45%`' in msg_mock.call_args_list[-1][0][0] assert '*Best Performing:* `ETH/USDT: 9.45%`' in msg_mock.call_args_list[-1][0][0]
assert '*Max Drawdown:*' in msg_mock.call_args_list[-1][0][0]
assert '*Profit factor:*' in msg_mock.call_args_list[-1][0][0]
assert '*Trading volume:* `60 USDT`' in msg_mock.call_args_list[-1][0][0]
@pytest.mark.parametrize('is_short', [True, False]) @pytest.mark.parametrize('is_short', [True, False])
def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None:
limit_buy_order, limit_sell_order, mocker, is_short) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -1686,7 +1688,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
f'\N{LARGE BLUE CIRCLE} *Binance:* {enter} ETH/BTC (#1)\n' f'\N{LARGE BLUE CIRCLE} *Binance (dry):* {enter} ETH/BTC (#1)\n'
f'*Enter Tag:* `{enter_signal}`\n' f'*Enter Tag:* `{enter_signal}`\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
f'{leverage_text}' f'{leverage_text}'
@ -1726,7 +1728,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, en
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'reason': CANCEL_REASON['TIMEOUT'] 'reason': CANCEL_REASON['TIMEOUT']
}) })
assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance:* ' assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance (dry):* '
'Cancelling enter Order for ETH/BTC (#1). ' 'Cancelling enter Order for ETH/BTC (#1). '
'Reason: cancelled due to timeout.') 'Reason: cancelled due to timeout.')
@ -1787,7 +1789,7 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en
}) })
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else '' leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
f'\N{CHECK MARK} *Binance:* {entered}ed ETH/BTC (#1)\n' f'\N{CHECK MARK} *Binance (dry):* {entered}ed ETH/BTC (#1)\n'
f'*Enter Tag:* `{enter_signal}`\n' f'*Enter Tag:* `{enter_signal}`\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
f"{leverage_text}" f"{leverage_text}"
@ -1814,7 +1816,7 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en
}) })
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
f'\N{CHECK MARK} *Binance:* {entered}ed ETH/BTC (#1)\n' f'\N{CHECK MARK} *Binance (dry):* {entered}ed ETH/BTC (#1)\n'
f'*Enter Tag:* `{enter_signal}`\n' f'*Enter Tag:* `{enter_signal}`\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
f"{leverage_text}" f"{leverage_text}"
@ -1852,7 +1854,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'close_date': arrow.utcnow(), 'close_date': arrow.utcnow(),
}) })
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n' '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
'*Enter Tag:* `buy_signal1`\n' '*Enter Tag:* `buy_signal1`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'
@ -1890,7 +1892,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'sub_trade': True 'sub_trade': True
}) })
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n' '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'*Unrealized Cumulative Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' '*Unrealized Cumulative Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
'*Enter Tag:* `buy_signal1`\n' '*Enter Tag:* `buy_signal1`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'
@ -1924,7 +1926,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'close_date': arrow.utcnow(), 'close_date': arrow.utcnow(),
}) })
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n' '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n'
'*Enter Tag:* `buy_signal1`\n' '*Enter Tag:* `buy_signal1`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'
@ -1953,10 +1955,12 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
'reason': 'Cancelled on exchange' 'reason': 'Cancelled on exchange'
}) })
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance:* Cancelling exit Order for KEY/ETH (#1).' '\N{WARNING SIGN} *Binance (dry):* Cancelling exit Order for KEY/ETH (#1).'
' Reason: Cancelled on exchange.') ' Reason: Cancelled on exchange.')
msg_mock.reset_mock() msg_mock.reset_mock()
# Test with live mode (no dry appendix)
telegram._config['dry_run'] = False
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.EXIT_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
'trade_id': 1, 'trade_id': 1,
@ -2005,7 +2009,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction,
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance:* Exited KEY/ETH (#1)\n' '\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n'
'*Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' '*Profit:* `-57.41% (loss: -0.05746268 ETH)`\n'
f'*Enter Tag:* `{enter_signal}`\n' f'*Enter Tag:* `{enter_signal}`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'
@ -2061,6 +2065,7 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None:
def test_send_msg_buy_notification_no_fiat( def test_send_msg_buy_notification_no_fiat(
default_conf, mocker, message_type, enter, enter_signal, leverage) -> None: default_conf, mocker, message_type, enter, enter_signal, leverage) -> None:
del default_conf['fiat_display_currency'] del default_conf['fiat_display_currency']
default_conf['dry_run'] = False
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({ telegram.send_msg({
@ -2130,7 +2135,7 @@ def test_send_msg_sell_notification_no_fiat(
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n' '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n'
f'*Enter Tag:* `{enter_signal}`\n' f'*Enter Tag:* `{enter_signal}`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'

View File

@ -2082,6 +2082,24 @@ def test_get_trades_proxy(fee, use_db, is_short):
Trade.use_db = True Trade.use_db = True
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('is_short', [True, False])
def test_get_trades__query(fee, is_short):
query = Trade.get_trades([])
# without orders there should be no join issued.
query1 = Trade.get_trades([], include_orders=False)
assert "JOIN orders" in str(query)
assert "JOIN orders" not in str(query1)
create_mock_trades(fee, is_short)
query = Trade.get_trades([])
query1 = Trade.get_trades([], include_orders=False)
assert "JOIN orders" in str(query)
assert "JOIN orders" not in str(query1)
def test_get_trades_backtest(): def test_get_trades_backtest():
Trade.use_db = False Trade.use_db = False
with pytest.raises(NotImplementedError, match=r"`Trade.get_trades\(\)` not .*"): with pytest.raises(NotImplementedError, match=r"`Trade.get_trades\(\)` not .*"):
@ -2276,6 +2294,7 @@ def test_Trade_object_idem():
'get_exit_reason_performance', 'get_exit_reason_performance',
'get_enter_tag_performance', 'get_enter_tag_performance',
'get_mix_tag_performance', 'get_mix_tag_performance',
'get_trading_volume',
) )