Merge branch 'freqtrade:develop' into develop

This commit is contained in:
Surfer 2022-06-21 14:16:03 -04:00 committed by GitHub
commit cc4e5b26f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 344 additions and 144 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

@ -64,7 +64,10 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
### Margin mode ### Margin mode
The possible values are: `isolated`, or `cross`(*currently unavailable*) On top of `trading_mode` - you will also have to configure your `margin_mode`.
While freqtrade currently only supports one margin mode, this will change, and by configuring it now you're all set for future updates.
The possible values are: `isolated`, or `cross`(*currently unavailable*).
#### Isolated margin mode #### Isolated margin mode
@ -82,6 +85,16 @@ One account is used to share collateral between markets (trading pairs). Margin
"margin_mode": "cross" "margin_mode": "cross"
``` ```
## Set leverage to use
Different strategies and risk profiles will require different levels of leverage.
While you could configure one static leverage value - freqtrade offers you the flexibility to adjust this via [strategy leverage callback](strategy-callbacks.md#leverage-callback) - which allows you to use different leverages by pair, or based on some other factor benefitting your strategy result.
If not implemented, leverage defaults to 1x (no leverage).
!!! Warning
Higher leverage also equals higher risk - be sure you fully understand the implications of using leverage!
## Understand `liquidation_buffer` ## Understand `liquidation_buffer`
*Defaults to `0.05`* *Defaults to `0.05`*

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

@ -191,6 +191,19 @@ For example, simplified math:
!!! Tip !!! Tip
Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade.
## Stoploss and Leverage
Stoploss should be thought of as "risk on this trade" - so a stoploss of 10% on a 100$ trade means you are willing to lose 10$ (10%) on this trade - which would trigger if the price moves 10% to the downside.
When using leverage, the same principle is applied - with stoploss defining the risk on the trade (the amount you are willing to lose).
Therefore, a stoploss of 10% on a 10x trade would trigger on a 1% price move.
If your stake amount (own capital) was 100$ - this trade would be 1000$ at 10x (after leverage).
If price moves 1% - you've lost 10$ of your own capital - therfore stoploss will trigger in this case.
Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little).
## Changing stoploss on open trades ## Changing stoploss on open trades
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works). A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works).

View File

@ -823,3 +823,6 @@ class AwesomeStrategy(IStrategy):
""" """
return 1.0 return 1.0
``` ```
All profit calculations include leverage. Stoploss / ROI also include leverage in their calculation.
Defining a stoploss of 10% at 10x leverage would trigger the stoploss with a 1% move to the downside.

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

@ -221,7 +221,7 @@ def _download_pair_history(pair: str, *,
prepend=prepend) prepend=prepend)
logger.info(f'({process}) - Download history data for "{pair}", {timeframe}, ' logger.info(f'({process}) - Download history data for "{pair}", {timeframe}, '
f'{candle_type} and store in {datadir}.' f'{candle_type} and store in {datadir}. '
f'From {format_ms_time(since_ms) if since_ms else "start"} to ' f'From {format_ms_time(since_ms) if since_ms else "start"} to '
f'{format_ms_time(until_ms) if until_ms else "now"}' f'{format_ms_time(until_ms) if until_ms else "now"}'
) )

View File

@ -93,7 +93,7 @@ class Exchange:
:return: None :return: None
""" """
self._api: ccxt.Exchange self._api: ccxt.Exchange
self._api_async: ccxt_async.Exchange self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {} self._markets: Dict = {}
self._trading_fees: Dict[str, Any] = {} self._trading_fees: Dict[str, Any] = {}
self._leverage_tiers: Dict[str, List[Dict]] = {} self._leverage_tiers: Dict[str, List[Dict]] = {}
@ -2131,10 +2131,11 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier @retrier_async
def get_market_leverage_tiers(self, symbol) -> List[Dict]: async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]]:
try: try:
return self._api.fetch_market_leverage_tiers(symbol) tier = await self._api_async.fetch_market_leverage_tiers(symbol)
return symbol, tier
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
@ -2168,8 +2169,14 @@ class Exchange:
f"Initializing leverage_tiers for {len(symbols)} markets. " f"Initializing leverage_tiers for {len(symbols)} markets. "
"This will take about a minute.") "This will take about a minute.")
for symbol in sorted(symbols): coros = [self.get_market_leverage_tiers(symbol) for symbol in sorted(symbols)]
tiers[symbol] = self.get_market_leverage_tiers(symbol)
for input_coro in chunks(coros, 100):
results = self.loop.run_until_complete(
asyncio.gather(*input_coro, return_exceptions=True))
for symbol, res in results:
tiers[symbol] = res
logger.info(f"Done initializing {len(symbols)} markets.") logger.info(f"Done initializing {len(symbols)} markets.")

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

@ -1055,6 +1055,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.
@ -1078,6 +1079,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.
@ -1085,7 +1088,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

@ -8,7 +8,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, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort
from freqtrade.enums import ExitType, TradingMode from freqtrade.enums import ExitType, TradingMode
@ -624,8 +624,8 @@ class LocalTrade():
""" """
self.close_rate = rate self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow() self.close_date = self.close_date or datetime.utcnow()
self.close_profit = self.calc_profit_ratio() self.close_profit = self.calc_profit_ratio(rate)
self.close_profit_abs = self.calc_profit() self.close_profit_abs = self.calc_profit(rate)
self.is_open = False self.is_open = False
self.exit_order_status = 'closed' self.exit_order_status = 'closed'
self.open_order_id = None self.open_order_id = None
@ -693,10 +693,9 @@ class LocalTrade():
""" """
self.open_trade_value = self._calc_open_trade_value() self.open_trade_value = self._calc_open_trade_value()
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal: def calculate_interest(self) -> Decimal:
""" """
:param interest_rate: interest_charge for borrowing this coin(optional). Calculate interest for this trade. Only applicable for Margin trading.
If interest_rate is not set self.interest_rate will be used
""" """
zero = Decimal(0.0) zero = Decimal(0.0)
# If nothing was borrowed # If nothing was borrowed
@ -709,34 +708,26 @@ class LocalTrade():
total_seconds = Decimal((now - open_date).total_seconds()) total_seconds = Decimal((now - open_date).total_seconds())
hours = total_seconds / sec_per_hour or zero hours = total_seconds / sec_per_hour or zero
rate = Decimal(interest_rate or self.interest_rate) rate = Decimal(self.interest_rate)
borrowed = Decimal(self.borrowed) borrowed = Decimal(self.borrowed)
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours) return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None, def _calc_base_close(self, amount: Decimal, rate: float, fee: float) -> Decimal:
fee: Optional[float] = None) -> Decimal:
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore close_trade = amount * Decimal(rate)
fees = close_trade * Decimal(fee or self.fee_close) fees = close_trade * Decimal(fee)
if self.is_short: if self.is_short:
return close_trade + fees return close_trade + fees
else: else:
return close_trade - fees return close_trade - fees
def calc_close_trade_value(self, rate: Optional[float] = None, def calc_close_trade_value(self, rate: float) -> float:
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
""" """
Calculate the close_rate including fee Calculate the Trade's close value including fees
:param fee: fee to use on the close rate (optional). :param rate: rate to compare with.
If rate is not set self.fee will be used :return: value in stake currency of the open trade
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: Price in BTC of the open trade
""" """
if rate is None and not self.close_rate: if rate is None and not self.close_rate:
return 0.0 return 0.0
@ -745,49 +736,38 @@ class LocalTrade():
trading_mode = self.trading_mode or TradingMode.SPOT trading_mode = self.trading_mode or TradingMode.SPOT
if trading_mode == TradingMode.SPOT: if trading_mode == TradingMode.SPOT:
return float(self._calc_base_close(amount, rate, fee)) return float(self._calc_base_close(amount, rate, self.fee_close))
elif (trading_mode == TradingMode.MARGIN): elif (trading_mode == TradingMode.MARGIN):
total_interest = self.calculate_interest(interest_rate) total_interest = self.calculate_interest()
if self.is_short: if self.is_short:
amount = amount + total_interest amount = amount + total_interest
return float(self._calc_base_close(amount, rate, fee)) return float(self._calc_base_close(amount, rate, self.fee_close))
else: else:
# Currency already owned for longs, no need to purchase # Currency already owned for longs, no need to purchase
return float(self._calc_base_close(amount, rate, fee) - total_interest) return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest)
elif (trading_mode == TradingMode.FUTURES): elif (trading_mode == TradingMode.FUTURES):
funding_fees = self.funding_fees or 0.0 funding_fees = self.funding_fees or 0.0
# Positive funding_fees -> Trade has gained from fees. # Positive funding_fees -> Trade has gained from fees.
# Negative funding_fees -> Trade had to pay the fees. # Negative funding_fees -> Trade had to pay the fees.
if self.is_short: if self.is_short:
return float(self._calc_base_close(amount, rate, fee)) - funding_fees return float(self._calc_base_close(amount, rate, self.fee_close)) - funding_fees
else: else:
return float(self._calc_base_close(amount, rate, fee)) + funding_fees return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees
else: else:
raise OperationalException( raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade") f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: Optional[float] = None, def calc_profit(self, rate: float) -> float:
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
""" """
Calculate the absolute profit in stake currency between Close and Open trade Calculate the absolute profit in stake currency between Close and Open trade
:param fee: fee to use on the close rate (optional). :param rate: close rate to compare with.
If fee is not set self.fee will be used :return: profit in stake currency as float
:param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit in stake currency as float
""" """
close_trade_value = self.calc_close_trade_value( close_trade_value = self.calc_close_trade_value(rate)
rate=(rate or self.close_rate),
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
if self.is_short: if self.is_short:
profit = self.open_trade_value - close_trade_value profit = self.open_trade_value - close_trade_value
@ -795,23 +775,13 @@ class LocalTrade():
profit = close_trade_value - self.open_trade_value profit = close_trade_value - self.open_trade_value
return float(f"{profit:.8f}") return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: Optional[float] = None, def calc_profit_ratio(self, rate: float) -> float:
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
""" """
Calculates the profit as ratio (including fee). Calculates the profit as ratio (including fee).
:param rate: rate to compare with (optional). :param rate: rate to compare with.
If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional).
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit ratio as float :return: profit ratio as float
""" """
close_trade_value = self.calc_close_trade_value( close_trade_value = self.calc_close_trade_value(rate)
rate=(rate or self.close_rate),
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
short_close_zero = (self.is_short and close_trade_value == 0.0) short_close_zero = (self.is_short and close_trade_value == 0.0)
long_close_zero = (not self.is_short and self.open_trade_value == 0.0) long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
@ -1145,7 +1115,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.
@ -1160,9 +1130,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']:
@ -1382,3 +1357,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,11 +531,15 @@ 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:
""" Returns current account balance per crypto """ """ Returns current account balance per crypto """
currencies = [] currencies: List[Dict] = []
total = 0.0 total = 0.0
try: try:
tickers = self._freqtrade.exchange.get_tickers(cached=True) tickers = self._freqtrade.exchange.get_tickers(cached=True)
@ -547,13 +574,12 @@ class RPC:
except (ExchangeError): except (ExchangeError):
logger.warning(f" Could not get rate for pair {coin}.") logger.warning(f" Could not get rate for pair {coin}.")
continue continue
total = total + (est_stake or 0) total = total + est_stake
currencies.append({ currencies.append({
'currency': coin, 'currency': coin,
# TODO: The below can be simplified if we don't assign None to values. 'free': balance.free,
'free': balance.free if balance.free is not None else 0, 'balance': balance.total,
'balance': balance.total if balance.total is not None else 0, 'used': balance.used,
'used': balance.used if balance.used is not None else 0,
'est_stake': est_stake or 0, 'est_stake': est_stake or 0,
'stake': stake_currency, 'stake': stake_currency,
'side': 'long', 'side': 'long',
@ -583,7 +609,6 @@ class RPC:
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
trade_count = len(Trade.get_trades_proxy()) trade_count = len(Trade.get_trades_proxy())
starting_capital_ratio = 0.0
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
@ -871,7 +896,7 @@ class RPC:
else: else:
errors[pair] = { errors[pair] = {
'error_msg': f"Pair {pair} is not in the current blacklist." 'error_msg': f"Pair {pair} is not in the current blacklist."
} }
resp = self._rpc_blacklist() resp = self._rpc_blacklist()
resp['errors'] = errors resp['errors'] = errors
return resp return resp

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"
) )
@ -298,7 +306,7 @@ class Telegram(RPCHandler):
msg['profit_extra'] = '' msg['profit_extra'] = ''
is_fill = msg['type'] == RPCMessageType.EXIT_FILL is_fill = msg['type'] == RPCMessageType.EXIT_FILL
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"
) )
if not is_fill and msg.get('analyzed_candle'): if not is_fill and msg.get('analyzed_candle'):
@ -334,33 +342,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:
@ -730,12 +738,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)
@ -875,7 +889,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:
@ -887,7 +901,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:
@ -899,7 +913,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:
@ -911,7 +925,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:
@ -1073,9 +1087,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))
@ -1403,7 +1417,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

@ -78,9 +78,21 @@ def get_args(args):
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
def get_mock_coro(return_value): # TODO: This should be replaced with AsyncMock once support for python 3.7 is dropped.
def get_mock_coro(return_value=None, side_effect=None):
async def mock_coro(*args, **kwargs): async def mock_coro(*args, **kwargs):
return return_value if side_effect:
if isinstance(side_effect, list):
effect = side_effect.pop(0)
else:
effect = side_effect
if isinstance(effect, Exception):
raise effect
if callable(effect):
return effect(*args, **kwargs)
return effect
else:
return return_value
return Mock(wraps=mock_coro) return Mock(wraps=mock_coro)

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

@ -6,7 +6,7 @@ import pytest
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.candletype import CandleType from freqtrade.enums.candletype import CandleType
from freqtrade.exchange.exchange import timeframe_to_minutes from freqtrade.exchange.exchange import timeframe_to_minutes
from tests.conftest import get_patched_exchange from tests.conftest import get_mock_coro, get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers from tests.exchange.test_exchange import ccxt_exceptionhandlers
@ -273,7 +273,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets):
'fetchLeverageTiers': False, 'fetchLeverageTiers': False,
'fetchMarketLeverageTiers': True, 'fetchMarketLeverageTiers': True,
}) })
api_mock.fetch_market_leverage_tiers = MagicMock(side_effect=[ api_mock.fetch_market_leverage_tiers = get_mock_coro(side_effect=[
[ [
{ {
'tier': 1, 'tier': 1,

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'],
} }
@ -852,8 +863,8 @@ def test_api_performance(botclient, fee):
close_rate=0.265441, close_rate=0.265441,
) )
trade.close_profit = trade.calc_profit_ratio() trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
trade.close_profit_abs = trade.calc_profit() trade.close_profit_abs = trade.calc_profit(trade.close_rate)
Trade.query.session.add(trade) Trade.query.session.add(trade)
trade = Trade( trade = Trade(
@ -868,8 +879,8 @@ def test_api_performance(botclient, fee):
fee_open=fee.return_value, fee_open=fee.return_value,
close_rate=0.391 close_rate=0.391
) )
trade.close_profit = trade.calc_profit_ratio() trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
trade.close_profit_abs = trade.calc_profit() trade.close_profit_abs = trade.calc_profit(trade.close_rate)
Trade.query.session.add(trade) Trade.query.session.add(trade)
Trade.commit() Trade.commit()

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',
@ -1680,7 +1682,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}'
@ -1720,7 +1722,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.')
@ -1782,7 +1784,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}"
@ -1820,7 +1822,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'
@ -1854,7 +1856,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%`\n' '*Unrealized Profit:* `-57.41%`\n'
'*Enter Tag:* `buy_signal1`\n' '*Enter Tag:* `buy_signal1`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'
@ -1883,10 +1885,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,
@ -1935,7 +1939,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%`\n' '*Profit:* `-57.41%`\n'
f'*Enter Tag:* `{enter_signal}`\n' f'*Enter Tag:* `{enter_signal}`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'
@ -1991,6 +1995,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({
@ -2060,7 +2065,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%`\n' '*Unrealized Profit:* `-57.41%`\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

@ -2151,7 +2151,7 @@ def test_handle_trade(
assert trade.close_rate == 2.0 if is_short else 2.2 assert trade.close_rate == 2.0 if is_short else 2.2
assert trade.close_profit == close_profit assert trade.close_profit == close_profit
assert trade.calc_profit() == 5.685 assert trade.calc_profit(trade.close_rate) == 5.685
assert trade.close_date is not None assert trade.close_date is not None
assert trade.exit_reason == 'sell_signal1' assert trade.exit_reason == 'sell_signal1'

View File

@ -606,9 +606,9 @@ def test_calc_open_close_trade_price(
trade.close_rate = 2.2 trade.close_rate = 2.2
trade.recalc_open_trade_value() trade.recalc_open_trade_value()
assert isclose(trade._calc_open_trade_value(), open_value) assert isclose(trade._calc_open_trade_value(), open_value)
assert isclose(trade.calc_close_trade_value(), close_value) assert isclose(trade.calc_close_trade_value(trade.close_rate), close_value)
assert isclose(trade.calc_profit(), round(profit, 8)) assert isclose(trade.calc_profit(trade.close_rate), round(profit, 8))
assert pytest.approx(trade.calc_profit_ratio()) == profit_ratio assert pytest.approx(trade.calc_profit_ratio(trade.close_rate)) == profit_ratio
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@ -660,7 +660,7 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee):
trade.open_order_id = 'something' trade.open_order_id = 'something'
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj) trade.update_trade(oobj)
assert trade.calc_close_trade_value() == 0.0 assert trade.calc_close_trade_value(trade.close_rate) == 0.0
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@ -813,7 +813,7 @@ def test_calc_close_trade_price(
funding_fees=funding_fees funding_fees=funding_fees
) )
trade.open_order_id = 'close_trade' trade.open_order_id = 'close_trade'
assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result assert round(trade.calc_close_trade_value(rate=close_rate), 8) == result
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -884,6 +884,17 @@ def test_calc_close_trade_price(
('binance', False, 3, 2.2, 0.0025, 4.684999, 0.23366583, futures, -1), ('binance', False, 3, 2.2, 0.0025, 4.684999, 0.23366583, futures, -1),
('binance', True, 1, 2.2, 0.0025, -7.315, -0.12222222, futures, -1), ('binance', True, 1, 2.2, 0.0025, -7.315, -0.12222222, futures, -1),
('binance', True, 3, 2.2, 0.0025, -7.315, -0.36666666, futures, -1), ('binance', True, 3, 2.2, 0.0025, -7.315, -0.36666666, futures, -1),
# FUTURES, funding_fee=0
('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309, futures, 0),
('binance', False, 3, 2.1, 0.0025, 2.6925, 0.13428928, futures, 0),
('binance', True, 1, 2.1, 0.0025, -3.3074999, -0.05526316, futures, 0),
('binance', True, 3, 2.1, 0.0025, -3.3074999, -0.16578947, futures, 0),
('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815, futures, 0),
('binance', False, 3, 1.9, 0.0025, -3.2925, -0.16421446, futures, 0),
('binance', True, 1, 1.9, 0.0025, 2.7075, 0.0452381, futures, 0),
('binance', True, 3, 1.9, 0.0025, 2.7075, 0.13571429, futures, 0),
]) ])
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_calc_profit( def test_calc_profit(
@ -2064,6 +2075,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 .*"):
@ -2258,6 +2287,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',
) )