Merge branch 'develop' into plugins/protections_backtest

This commit is contained in:
Matthias 2020-12-13 10:31:33 +01:00
commit 7eab33de08
24 changed files with 332 additions and 93 deletions

View File

@ -1,24 +1,41 @@
FROM python:3.8.6-slim-buster
FROM python:3.8.6-slim-buster as base
RUN apt-get update \
&& apt-get -y install curl build-essential libssl-dev sqlite3 \
&& apt-get clean \
&& pip install --upgrade pip
# Setup env
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONFAULTHANDLER 1
ENV PATH=/root/.local/bin:$PATH
# Prepare environment
RUN mkdir /freqtrade
WORKDIR /freqtrade
# Install dependencies
FROM base as python-deps
RUN apt-get update \
&& apt-get -y install curl build-essential libssl-dev git \
&& apt-get clean \
&& pip install --upgrade pip
# Install TA-lib
COPY build_helpers/* /tmp/
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
ENV LD_LIBRARY_PATH /usr/local/lib
# Install dependencies
COPY requirements.txt requirements-hyperopt.txt /freqtrade/
RUN pip install numpy --no-cache-dir \
&& pip install -r requirements-hyperopt.txt --no-cache-dir
RUN pip install --user --no-cache-dir numpy \
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt
# Copy dependencies to runtime-image
FROM base as runtime-image
COPY --from=python-deps /usr/local/lib /usr/local/lib
ENV LD_LIBRARY_PATH /usr/local/lib
COPY --from=python-deps /root/.local /root/.local
# Install and execute
COPY . /freqtrade/

View File

@ -285,7 +285,7 @@ Since backtesting lacks some detailed information about what happens within a ca
- sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%)
- sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit
- Forcesells caused by `<N>=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
- Stoploss sells happen exactly at stoploss price, even if low was lower
- Stoploss sells happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes
- Low happens before high for stoploss, protecting capital first
- Trailing stoploss

View File

@ -8,7 +8,7 @@ If no additional parameter is specified, freqtrade will download data for `"1m"`
Exchange and pairs will come from `config.json` (if specified using `-c/--config`).
Otherwise `--exchange` becomes mandatory.
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101`). For incremental downloads, the relative approach should be used.
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
!!! Tip "Tip: Updating existing data"
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data.

View File

@ -112,7 +112,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
``` bash
sudo apt-get install python3-venv libatlas-base-dev
sudo apt-get install python3-venv libatlas-base-dev cmake
# Use pywheels.org to speed up installation
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf

View File

@ -1,3 +1,3 @@
mkdocs-material==6.1.6
mkdocs-material==6.1.7
mdx_truly_sane_lists==1.2
pymdown-extensions==8.0.1

View File

@ -127,6 +127,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `performance` | Show performance of each finished trade grouped by pair.
| `balance` | Show account balance per currency.
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
| `stats` | Display a summary of profit / loss reasons as well as average holding times.
| `whitelist` | Show the current whitelist.
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
| `edge` | Show validated pairs by Edge if it is enabled.
@ -229,6 +230,9 @@ show_config
start
Start the bot if it's in the stopped state.
stats
Return the stats report (durations, sell-reasons).
status
Get the status of open trades.

View File

@ -113,6 +113,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/performance` | Show performance of each finished trade grouped by pair
| `/balance` | Show account balance per currency
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
| `/stats` | Shows Wins / losses by Sell reason as well as Avg. holding durations for buys and sells
| `/whitelist` | Show the current whitelist
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
| `/edge` | Show validated pairs by Edge if it is enabled.

View File

@ -532,7 +532,6 @@ class FreqtradeBot(LoggingMixin):
# reserve some percent defined in config (5% default) + stoploss
amount_reserve_percent = 1.0 - self.config.get('amount_reserve_percent',
constants.DEFAULT_AMOUNT_RESERVE_PERCENT)
if self.strategy.stoploss is not None:
amount_reserve_percent += self.strategy.stoploss
# it should not be more than 50%
amount_reserve_percent = max(amount_reserve_percent, 0.5)
@ -1415,7 +1414,7 @@ class FreqtradeBot(LoggingMixin):
abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount
order.pop('filled', None)
trade.recalc_open_trade_price()
trade.recalc_open_trade_value()
except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception)
@ -1470,7 +1469,10 @@ class FreqtradeBot(LoggingMixin):
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
if fee_rate is None or fee_rate < 0.02:
# Reject all fees that report as > 2%.
# These are most likely caused by a parsing bug in ccxt
# due to multiple trades (https://github.com/ccxt/ccxt/issues/8025)
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
if trade_base_currency == fee_currency:
# Apply fee to amount

View File

@ -53,11 +53,11 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
else:
timeframe = get_column_def(cols, 'timeframe', 'null')
open_trade_price = get_column_def(cols, 'open_trade_price',
open_trade_value = get_column_def(cols, 'open_trade_value',
f'amount * open_rate * (1 + {fee_open})')
close_profit_abs = get_column_def(
cols, 'close_profit_abs',
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}")
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}")
sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
amount_requested = get_column_def(cols, 'amount_requested', 'amount')
@ -79,7 +79,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy,
timeframe, open_trade_price, close_profit_abs
timeframe, open_trade_value, close_profit_abs
)
select id, lower(exchange),
case
@ -102,7 +102,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
{sell_order_status} sell_order_status,
{strategy} strategy, {timeframe} timeframe,
{open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
from {table_back_name}
""")
@ -134,7 +134,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
table_back_name = get_backup_name(tabs, 'trades_bak')
# Check for latest column
if not has_column(cols, 'amount_requested'):
if not has_column(cols, 'open_trade_value'):
logger.info(f'Running database migration for trades - backup: {table_back_name}')
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
# Reread columns - the above recreated the table!

View File

@ -221,8 +221,8 @@ class Trade(_DECL_BASE):
fee_close_currency = Column(String, nullable=True)
open_rate = Column(Float)
open_rate_requested = Column(Float)
# open_trade_price - calculated via _calc_open_trade_price
open_trade_price = Column(Float)
# open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float)
close_rate = Column(Float)
close_rate_requested = Column(Float)
close_profit = Column(Float)
@ -256,7 +256,7 @@ class Trade(_DECL_BASE):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.recalc_open_trade_price()
self.recalc_open_trade_value()
def __repr__(self):
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
@ -288,7 +288,7 @@ class Trade(_DECL_BASE):
'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000),
'open_rate': self.open_rate,
'open_rate_requested': self.open_rate_requested,
'open_trade_price': round(self.open_trade_price, 8),
'open_trade_value': round(self.open_trade_value, 8),
'close_date_hum': (arrow.get(self.close_date).humanize()
if self.close_date else None),
@ -401,7 +401,7 @@ class Trade(_DECL_BASE):
# Update open rate and actual amount
self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price'))
self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount'))
self.recalc_open_trade_price()
self.recalc_open_trade_value()
if self.is_open:
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.')
self.open_order_id = None
@ -477,7 +477,7 @@ class Trade(_DECL_BASE):
Trade.session.delete(self)
Trade.session.flush()
def _calc_open_trade_price(self) -> float:
def _calc_open_trade_value(self) -> float:
"""
Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees
@ -486,14 +486,14 @@ class Trade(_DECL_BASE):
fees = buy_trade * Decimal(self.fee_open)
return float(buy_trade + fees)
def recalc_open_trade_price(self) -> None:
def recalc_open_trade_value(self) -> None:
"""
Recalculate open_trade_price.
Recalculate open_trade_value.
Must be called whenever open_rate or fee_open is changed.
"""
self.open_trade_price = self._calc_open_trade_price()
self.open_trade_value = self._calc_open_trade_value()
def calc_close_trade_price(self, rate: Optional[float] = None,
def calc_close_trade_value(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
"""
Calculate the close_rate including fee
@ -520,11 +520,11 @@ class Trade(_DECL_BASE):
If rate is not set self.close_rate will be used
:return: profit in stake currency as float
"""
close_trade_price = self.calc_close_trade_price(
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
)
profit = close_trade_price - self.open_trade_price
profit = close_trade_value - self.open_trade_value
return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: Optional[float] = None,
@ -536,11 +536,11 @@ class Trade(_DECL_BASE):
:param fee: fee to use on the close rate (optional).
:return: profit ratio as float
"""
close_trade_price = self.calc_close_trade_price(
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
)
profit_ratio = (close_trade_price / self.open_trade_price) - 1
profit_ratio = (close_trade_value / self.open_trade_value) - 1
return float(f"{profit_ratio:.8f}")
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:

View File

@ -88,9 +88,6 @@ class StrategyResolver(IResolver):
StrategyResolver._override_attribute_helper(strategy, config,
attribute, default)
# Assign deprecated variable - to not break users code relying on this.
strategy.ticker_interval = strategy.timeframe
# Loop this list again to have output combined
for attribute, _, subkey in attributes:
if subkey and attribute in config[subkey]:
@ -98,11 +95,7 @@ class StrategyResolver(IResolver):
elif attribute in config:
logger.info("Strategy using %s: %s", attribute, config[attribute])
# Sort and apply type conversions
strategy.minimal_roi = OrderedDict(sorted(
{int(key): value for (key, value) in strategy.minimal_roi.items()}.items(),
key=lambda t: t[0]))
strategy.stoploss = float(strategy.stoploss)
StrategyResolver._normalize_attributes(strategy)
StrategyResolver._strategy_sanity_validations(strategy)
return strategy
@ -131,6 +124,24 @@ class StrategyResolver(IResolver):
setattr(strategy, attribute, default)
config[attribute] = default
@staticmethod
def _normalize_attributes(strategy: IStrategy) -> IStrategy:
"""
Normalize attributes to have the correct type.
"""
# Assign deprecated variable - to not break users code relying on this.
if hasattr(strategy, 'timeframe'):
strategy.ticker_interval = strategy.timeframe
# Sort and apply type conversions
if hasattr(strategy, 'minimal_roi'):
strategy.minimal_roi = OrderedDict(sorted(
{int(key): value for (key, value) in strategy.minimal_roi.items()}.items(),
key=lambda t: t[0]))
if hasattr(strategy, 'stoploss'):
strategy.stoploss = float(strategy.stoploss)
return strategy
@staticmethod
def _strategy_sanity_validations(strategy):
if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES):

View File

@ -198,6 +198,8 @@ class ApiServer(RPC):
self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/profit', 'profit',
view_func=self._profit, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/stats', 'stats',
view_func=self._stats, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/performance', 'performance',
view_func=self._performance, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/status', 'status',
@ -417,6 +419,18 @@ class ApiServer(RPC):
return jsonify(stats)
@require_login
@rpc_catch_errors
def _stats(self):
"""
Handler for /stats.
Returns a Object with "durations" and "sell_reasons" as keys.
"""
stats = self._rpc_stats()
return jsonify(stats)
@require_login
@rpc_catch_errors
def _performance(self):

View File

@ -275,6 +275,39 @@ class RPC:
"trades_count": len(output)
}
def _rpc_stats(self) -> Dict[str, Any]:
"""
Generate generic stats for trades in database
"""
def trade_win_loss(trade):
if trade.close_profit > 0:
return 'wins'
elif trade.close_profit < 0:
return 'losses'
else:
return 'draws'
trades = trades = Trade.get_trades([Trade.is_open.is_(False)])
# Sell reason
sell_reasons = {}
for trade in trades:
if trade.sell_reason not in sell_reasons:
sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1
# Duration
dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []}
for trade in trades:
if trade.close_date is not None and trade.open_date is not None:
trade_dur = (trade.close_date - trade.open_date).total_seconds()
dur[trade_win_loss(trade)].append(trade_dur)
wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else 'N/A'
draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else 'N/A'
losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A'
durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur}
return {'sell_reasons': sell_reasons, 'durations': durations}
def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
""" Returns cumulative profit statistics """

View File

@ -5,6 +5,7 @@ This module manage Telegram communication
"""
import json
import logging
from datetime import timedelta
from typing import Any, Callable, Dict, List, Union
import arrow
@ -98,6 +99,7 @@ class Telegram(RPC):
CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade),
CommandHandler('performance', self._performance),
CommandHandler('stats', self._stats),
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
CommandHandler('locks', self._locks),
@ -388,6 +390,48 @@ class Telegram(RPC):
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
self._send_msg(markdown_msg)
@authorized_only
def _stats(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /stats
Show stats of recent trades
"""
stats = self._rpc_stats()
reason_map = {
'roi': 'ROI',
'stop_loss': 'Stoploss',
'trailing_stop_loss': 'Trail. Stop',
'stoploss_on_exchange': 'Stoploss',
'sell_signal': 'Sell Signal',
'force_sell': 'Forcesell',
'emergency_sell': 'Emergency Sell',
}
sell_reasons_tabulate = [
[
reason_map.get(reason, reason),
sum(count.values()),
count['wins'],
count['losses']
] for reason, count in stats['sell_reasons'].items()
]
sell_reasons_msg = tabulate(
sell_reasons_tabulate,
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
)
durations = stats['durations']
duration_msg = tabulate([
['Wins', str(timedelta(seconds=durations['wins']))
if durations['wins'] != 'N/A' else 'N/A'],
['Losses', str(timedelta(seconds=durations['losses']))
if durations['losses'] != 'N/A' else 'N/A']
],
headers=['', 'Avg. Duration']
)
msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""")
self._send_msg(msg, ParseMode.MARKDOWN)
@authorized_only
def _balance(self, update: Update, context: CallbackContext) -> None:
""" Handler for /balance """
@ -743,6 +787,8 @@ class Telegram(RPC):
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
"*/stats:* `Shows Wins / losses by Sell reason as well as "
"Avg. holding durationsfor buys and sells.`\n"
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
"*/locks:* `Show currently locked pairs`\n"
"*/balance:* `Show account balance per currency`\n"

View File

@ -551,8 +551,7 @@ class IStrategy(ABC):
# evaluate if the stoploss was hit if stoploss is not on exchange
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
# regular stoploss handling.
if ((self.stoploss is not None) and
(trade.stop_loss >= current_rate) and
if ((trade.stop_loss >= current_rate) and
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
sell_type = SellType.STOP_LOSS

View File

@ -1,7 +1,7 @@
numpy==1.19.4
pandas==1.1.4
ccxt==1.38.55
ccxt==1.39.10
aiohttp==3.7.3
SQLAlchemy==1.3.20
python-telegram-bot==13.1

View File

@ -139,6 +139,13 @@ class FtRestClient():
"""
return self._get("profit")
def stats(self):
"""Return the stats report (durations, sell-reasons).
:return: json object
"""
return self._get("stats")
def performance(self):
"""Return the performance of the different coins.

View File

@ -1588,16 +1588,7 @@ def fetch_trades_result():
@pytest.fixture(scope="function")
def trades_for_order2():
return [{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
return [{'info': {},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
@ -1609,16 +1600,7 @@ def trades_for_order2():
'cost': 1.963528,
'amount': 4.0,
'fee': {'cost': 0.004, 'currency': 'LTC'}},
{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
{'info': {},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
@ -1632,6 +1614,14 @@ def trades_for_order2():
'fee': {'cost': 0.004, 'currency': 'LTC'}}]
@pytest.fixture(scope="function")
def trades_for_order3(trades_for_order2):
# Different fee currencies for each trade
trades_for_order = deepcopy(trades_for_order2)
trades_for_order[0]['fee'] = {'cost': 0.02, 'currency': 'BNB'}
return trades_for_order
@pytest.fixture
def buy_order_fee():
return {

View File

@ -1,3 +1,5 @@
from datetime import datetime, timedelta, timezone
from freqtrade.persistence.models import Order, Trade
@ -82,6 +84,9 @@ def mock_trade_2(fee):
is_open=False,
open_order_id='dry_run_sell_12345',
strategy='DefaultStrategy',
sell_reason='sell_signal',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc),
)
o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy')
trade.orders.append(o)
@ -134,6 +139,9 @@ def mock_trade_3(fee):
close_profit=0.01,
exchange='bittrex',
is_open=False,
sell_reason='roi',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc),
)
o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy')
trade.orders.append(o)

View File

@ -62,7 +62,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY,
'open_trade_price': 0.0010025,
'open_trade_value': 0.0010025,
'close_rate_requested': ANY,
'sell_reason': ANY,
'sell_order_status': ANY,
@ -127,7 +127,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY,
'open_trade_price': ANY,
'open_trade_value': ANY,
'close_rate_requested': ANY,
'sell_reason': ANY,
'sell_order_status': ANY,

View File

@ -559,6 +559,35 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
}
@pytest.mark.usefixtures("init_persistence")
def test_api_stats(botclient, mocker, ticker, fee, markets,):
ftbot, client = botclient
patch_get_signal(ftbot, (True, False))
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker,
get_fee=fee,
markets=PropertyMock(return_value=markets)
)
rc = client_get(client, f"{BASE_URI}/stats")
assert_response(rc, 200)
assert 'durations' in rc.json
assert 'sell_reasons' in rc.json
create_mock_trades(fee)
rc = client_get(client, f"{BASE_URI}/stats")
assert_response(rc, 200)
assert 'durations' in rc.json
assert 'sell_reasons' in rc.json
assert 'wins' in rc.json['durations']
assert 'losses' in rc.json['durations']
assert 'draws' in rc.json['durations']
def test_api_performance(botclient, mocker, ticker, fee):
ftbot, client = botclient
patch_get_signal(ftbot, (True, False))
@ -678,7 +707,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'min_rate': 1.098e-05,
'open_order_id': None,
'open_rate_requested': 1.098e-05,
'open_trade_price': 0.0010025,
'open_trade_value': 0.0010025,
'sell_reason': None,
'sell_order_status': None,
'strategy': 'DefaultStrategy',
@ -805,7 +834,7 @@ def test_api_forcebuy(botclient, mocker, fee):
'min_rate': None,
'open_order_id': '123456',
'open_rate_requested': None,
'open_trade_price': 0.24605460,
'open_trade_value': 0.24605460,
'sell_reason': None,
'sell_order_status': None,
'strategy': None,

View File

@ -74,9 +74,10 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
"['delete'], ['performance'], ['daily'], ['count'], ['locks'], "
"['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], "
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], "
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']]")
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']"
"]")
assert log_has(message_str, caplog)
@ -468,6 +469,41 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
)
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot)
telegram._stats(update=update, context=MagicMock())
assert msg_mock.call_count == 1
# assert 'No trades yet.' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
# Create some test data
create_mock_trades(fee)
telegram._stats(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'Sell Reason' in msg_mock.call_args_list[-1][0][0]
assert 'ROI' in msg_mock.call_args_list[-1][0][0]
assert 'Avg. Duration' in msg_mock.call_args_list[-1][0][0]
msg_mock.reset_mock()
def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None:
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)

View File

@ -3744,6 +3744,48 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
caplog)
assert trade.fee_open == 0.001
assert trade.fee_close == 0.001
assert trade.fee_open_cost is not None
assert trade.fee_open_currency is not None
assert trade.fee_close_cost is None
assert trade.fee_close_currency is None
def test_get_real_amount_multi2(default_conf, trades_for_order3, buy_order_fee, caplog, fee,
mocker, markets):
# Different fee currency on both trades
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order3)
amount = float(sum(x['amount'] for x in trades_for_order3))
default_conf['stake_currency'] = 'ETH'
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.245441,
open_order_id="123456"
)
# Fake markets entry to enable fee parsing
markets['BNB/ETH'] = markets['ETH/BTC']
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
return_value={'ask': 0.19, 'last': 0.2})
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.0005)
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).',
caplog)
# Overall fee is average of both trade's fee
assert trade.fee_open == 0.001518575
assert trade.fee_open_cost is not None
assert trade.fee_open_currency is not None
assert trade.fee_close_cost is None
assert trade.fee_close_currency is None
def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, fee,
caplog, mocker):
@ -4290,7 +4332,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee):
freqtrade = get_patched_freqtradebot(mocker, default_conf)
def patch_with_fee(order):
order.update({'fee': {'cost': 0.1, 'rate': 0.2,
order.update({'fee': {'cost': 0.1, 'rate': 0.01,
'currency': order['symbol'].split('/')[0]}})
return order

View File

@ -177,10 +177,10 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade._calc_open_trade_price() == 0.0010024999999225068
assert trade._calc_open_trade_value() == 0.0010024999999225068
trade.update(limit_sell_order)
assert trade.calc_close_trade_price() == 0.0010646656050132426
assert trade.calc_close_trade_value() == 0.0010646656050132426
# Profit in BTC
assert trade.calc_profit() == 0.00006217
@ -233,7 +233,7 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee):
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade.calc_close_trade_price() == 0.0
assert trade.calc_close_trade_value() == 0.0
@pytest.mark.usefixtures("init_persistence")
@ -277,7 +277,7 @@ def test_update_invalid_order(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_trade_price(limit_buy_order, fee):
def test_calc_open_trade_value(limit_buy_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
@ -291,10 +291,10 @@ def test_calc_open_trade_price(limit_buy_order, fee):
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get the open rate price with the standard fee rate
assert trade._calc_open_trade_price() == 0.0010024999999225068
assert trade._calc_open_trade_value() == 0.0010024999999225068
trade.fee_open = 0.003
# Get the open rate price with a custom fee rate
assert trade._calc_open_trade_price() == 0.001002999999922468
assert trade._calc_open_trade_value() == 0.001002999999922468
@pytest.mark.usefixtures("init_persistence")
@ -312,14 +312,14 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get the close rate price with a custom close rate and a regular fee rate
assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318470471794
assert trade.calc_close_trade_value(rate=0.00001234) == 0.0011200318470471794
# Get the close rate price with a custom close rate and a custom fee rate
assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704275749754
assert trade.calc_close_trade_value(rate=0.00001234, fee=0.003) == 0.0011194704275749754
# Test when we apply a Sell order, and ask price with a custom fee rate
trade.update(limit_sell_order)
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972701635854
assert trade.calc_close_trade_value(fee=0.005) == 0.0010619972701635854
@pytest.mark.usefixtures("init_persistence")
@ -499,7 +499,7 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.open_trade_price == trade._calc_open_trade_price()
assert trade.open_trade_value == trade._calc_open_trade_value()
assert trade.close_profit_abs is None
assert trade.fee_open_cost is None
assert trade.fee_open_currency is None
@ -607,7 +607,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert log_has("trying trades_bak1", caplog)
assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration for trades - backup: trades_bak2", caplog)
assert trade.open_trade_price == trade._calc_open_trade_price()
assert trade.open_trade_value == trade._calc_open_trade_value()
assert trade.close_profit_abs is None
assert log_has("Moving open orders to Orders table.", caplog)
@ -677,7 +677,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.open_trade_price == trade._calc_open_trade_price()
assert trade.open_trade_value == trade._calc_open_trade_value()
assert log_has("trying trades_bak0", caplog)
assert log_has("Running database migration for trades - backup: trades_bak0", caplog)
@ -803,7 +803,7 @@ def test_to_json(default_conf, fee):
'close_timestamp': None,
'open_rate': 0.123,
'open_rate_requested': None,
'open_trade_price': 15.1668225,
'open_trade_value': 15.1668225,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
@ -896,7 +896,7 @@ def test_to_json(default_conf, fee):
'min_rate': None,
'open_order_id': None,
'open_rate_requested': None,
'open_trade_price': 12.33075,
'open_trade_value': 12.33075,
'sell_reason': None,
'sell_order_status': None,
'strategy': None,