Merge branch 'develop' into plugins/protections_backtest
This commit is contained in:
commit
7eab33de08
33
Dockerfile
33
Dockerfile
@ -1,24 +1,41 @@
|
|||||||
FROM python:3.8.6-slim-buster
|
FROM python:3.8.6-slim-buster as base
|
||||||
|
|
||||||
RUN apt-get update \
|
# Setup env
|
||||||
&& apt-get -y install curl build-essential libssl-dev sqlite3 \
|
ENV LANG C.UTF-8
|
||||||
&& apt-get clean \
|
ENV LC_ALL C.UTF-8
|
||||||
&& pip install --upgrade pip
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONFAULTHANDLER 1
|
||||||
|
ENV PATH=/root/.local/bin:$PATH
|
||||||
|
|
||||||
# Prepare environment
|
# Prepare environment
|
||||||
RUN mkdir /freqtrade
|
RUN mkdir /freqtrade
|
||||||
WORKDIR /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
|
# Install TA-lib
|
||||||
COPY build_helpers/* /tmp/
|
COPY build_helpers/* /tmp/
|
||||||
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
|
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
|
||||||
|
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY requirements.txt requirements-hyperopt.txt /freqtrade/
|
COPY requirements.txt requirements-hyperopt.txt /freqtrade/
|
||||||
RUN pip install numpy --no-cache-dir \
|
RUN pip install --user --no-cache-dir numpy \
|
||||||
&& pip install -r requirements-hyperopt.txt --no-cache-dir
|
&& 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
|
# Install and execute
|
||||||
COPY . /freqtrade/
|
COPY . /freqtrade/
|
||||||
|
@ -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 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
|
- 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)
|
- 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
|
- 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
|
- Low happens before high for stoploss, protecting capital first
|
||||||
- Trailing stoploss
|
- Trailing stoploss
|
||||||
|
@ -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`).
|
Exchange and pairs will come from `config.json` (if specified using `-c/--config`).
|
||||||
Otherwise `--exchange` becomes mandatory.
|
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"
|
!!! 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.
|
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.
|
||||||
|
@ -112,7 +112,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
|
|||||||
|
|
||||||
|
|
||||||
``` bash
|
``` 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
|
# Use pywheels.org to speed up installation
|
||||||
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
|
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
mkdocs-material==6.1.6
|
mkdocs-material==6.1.7
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==8.0.1
|
pymdown-extensions==8.0.1
|
||||||
|
@ -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.
|
| `performance` | Show performance of each finished trade grouped by pair.
|
||||||
| `balance` | Show account balance per currency.
|
| `balance` | Show account balance per currency.
|
||||||
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
|
| `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.
|
| `whitelist` | Show the current whitelist.
|
||||||
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||||
| `edge` | Show validated pairs by Edge if it is enabled.
|
| `edge` | Show validated pairs by Edge if it is enabled.
|
||||||
@ -229,6 +230,9 @@ show_config
|
|||||||
start
|
start
|
||||||
Start the bot if it's in the stopped state.
|
Start the bot if it's in the stopped state.
|
||||||
|
|
||||||
|
stats
|
||||||
|
Return the stats report (durations, sell-reasons).
|
||||||
|
|
||||||
status
|
status
|
||||||
Get the status of open trades.
|
Get the status of open trades.
|
||||||
|
|
||||||
|
@ -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
|
| `/performance` | Show performance of each finished trade grouped by pair
|
||||||
| `/balance` | Show account balance per currency
|
| `/balance` | Show account balance per currency
|
||||||
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
| `/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
|
| `/whitelist` | Show the current whitelist
|
||||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||||
| `/edge` | Show validated pairs by Edge if it is enabled.
|
| `/edge` | Show validated pairs by Edge if it is enabled.
|
||||||
|
@ -532,8 +532,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# reserve some percent defined in config (5% default) + stoploss
|
# reserve some percent defined in config (5% default) + stoploss
|
||||||
amount_reserve_percent = 1.0 - self.config.get('amount_reserve_percent',
|
amount_reserve_percent = 1.0 - self.config.get('amount_reserve_percent',
|
||||||
constants.DEFAULT_AMOUNT_RESERVE_PERCENT)
|
constants.DEFAULT_AMOUNT_RESERVE_PERCENT)
|
||||||
if self.strategy.stoploss is not None:
|
amount_reserve_percent += self.strategy.stoploss
|
||||||
amount_reserve_percent += self.strategy.stoploss
|
|
||||||
# it should not be more than 50%
|
# it should not be more than 50%
|
||||||
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
||||||
|
|
||||||
@ -1415,7 +1414,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
abs_tol=constants.MATH_CLOSE_PREC):
|
abs_tol=constants.MATH_CLOSE_PREC):
|
||||||
order['amount'] = new_amount
|
order['amount'] = new_amount
|
||||||
order.pop('filled', None)
|
order.pop('filled', None)
|
||||||
trade.recalc_open_trade_price()
|
trade.recalc_open_trade_value()
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
logger.warning("Could not update trade amount: %s", exception)
|
logger.warning("Could not update trade amount: %s", exception)
|
||||||
|
|
||||||
@ -1470,13 +1469,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
|
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
|
||||||
logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
|
logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
|
||||||
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
|
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
|
||||||
|
if fee_rate is None or fee_rate < 0.02:
|
||||||
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
|
# Reject all fees that report as > 2%.
|
||||||
if trade_base_currency == fee_currency:
|
# These are most likely caused by a parsing bug in ccxt
|
||||||
# Apply fee to amount
|
# due to multiple trades (https://github.com/ccxt/ccxt/issues/8025)
|
||||||
return self.apply_fee_conditional(trade, trade_base_currency,
|
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
|
||||||
amount=order_amount, fee_abs=fee_cost)
|
if trade_base_currency == fee_currency:
|
||||||
return order_amount
|
# Apply fee to amount
|
||||||
|
return self.apply_fee_conditional(trade, trade_base_currency,
|
||||||
|
amount=order_amount, fee_abs=fee_cost)
|
||||||
|
return order_amount
|
||||||
return self.fee_detection_from_trades(trade, order, order_amount)
|
return self.fee_detection_from_trades(trade, order, order_amount)
|
||||||
|
|
||||||
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float:
|
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float:
|
||||||
|
@ -53,11 +53,11 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
else:
|
else:
|
||||||
timeframe = get_column_def(cols, 'timeframe', 'null')
|
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})')
|
f'amount * open_rate * (1 + {fee_open})')
|
||||||
close_profit_abs = get_column_def(
|
close_profit_abs = get_column_def(
|
||||||
cols, 'close_profit_abs',
|
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')
|
sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
|
||||||
amount_requested = get_column_def(cols, 'amount_requested', 'amount')
|
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,
|
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
||||||
stoploss_order_id, stoploss_last_update,
|
stoploss_order_id, stoploss_last_update,
|
||||||
max_rate, min_rate, sell_reason, sell_order_status, strategy,
|
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),
|
select id, lower(exchange),
|
||||||
case
|
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,
|
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
||||||
{sell_order_status} sell_order_status,
|
{sell_order_status} sell_order_status,
|
||||||
{strategy} strategy, {timeframe} timeframe,
|
{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}
|
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')
|
table_back_name = get_backup_name(tabs, 'trades_bak')
|
||||||
|
|
||||||
# Check for latest column
|
# 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}')
|
logger.info(f'Running database migration for trades - backup: {table_back_name}')
|
||||||
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
|
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
|
||||||
# Reread columns - the above recreated the table!
|
# Reread columns - the above recreated the table!
|
||||||
|
@ -221,8 +221,8 @@ class Trade(_DECL_BASE):
|
|||||||
fee_close_currency = Column(String, nullable=True)
|
fee_close_currency = Column(String, nullable=True)
|
||||||
open_rate = Column(Float)
|
open_rate = Column(Float)
|
||||||
open_rate_requested = Column(Float)
|
open_rate_requested = Column(Float)
|
||||||
# open_trade_price - calculated via _calc_open_trade_price
|
# open_trade_value - calculated via _calc_open_trade_value
|
||||||
open_trade_price = Column(Float)
|
open_trade_value = Column(Float)
|
||||||
close_rate = Column(Float)
|
close_rate = Column(Float)
|
||||||
close_rate_requested = Column(Float)
|
close_rate_requested = Column(Float)
|
||||||
close_profit = Column(Float)
|
close_profit = Column(Float)
|
||||||
@ -256,7 +256,7 @@ class Trade(_DECL_BASE):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.recalc_open_trade_price()
|
self.recalc_open_trade_value()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
|
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_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000),
|
||||||
'open_rate': self.open_rate,
|
'open_rate': self.open_rate,
|
||||||
'open_rate_requested': self.open_rate_requested,
|
'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()
|
'close_date_hum': (arrow.get(self.close_date).humanize()
|
||||||
if self.close_date else None),
|
if self.close_date else None),
|
||||||
@ -401,7 +401,7 @@ class Trade(_DECL_BASE):
|
|||||||
# Update open rate and actual amount
|
# Update open rate and actual amount
|
||||||
self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price'))
|
self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price'))
|
||||||
self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount'))
|
self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount'))
|
||||||
self.recalc_open_trade_price()
|
self.recalc_open_trade_value()
|
||||||
if self.is_open:
|
if self.is_open:
|
||||||
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.')
|
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.')
|
||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
@ -477,7 +477,7 @@ class Trade(_DECL_BASE):
|
|||||||
Trade.session.delete(self)
|
Trade.session.delete(self)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
def _calc_open_trade_price(self) -> float:
|
def _calc_open_trade_value(self) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the open_rate including open_fee.
|
Calculate the open_rate including open_fee.
|
||||||
:return: Price in of the open trade incl. Fees
|
:return: Price in of the open trade incl. Fees
|
||||||
@ -486,14 +486,14 @@ class Trade(_DECL_BASE):
|
|||||||
fees = buy_trade * Decimal(self.fee_open)
|
fees = buy_trade * Decimal(self.fee_open)
|
||||||
return float(buy_trade + fees)
|
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.
|
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:
|
fee: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the close_rate including fee
|
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
|
If rate is not set self.close_rate will be used
|
||||||
:return: profit in stake currency as float
|
: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),
|
rate=(rate or self.close_rate),
|
||||||
fee=(fee or self.fee_close)
|
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}")
|
return float(f"{profit:.8f}")
|
||||||
|
|
||||||
def calc_profit_ratio(self, rate: Optional[float] = None,
|
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).
|
:param fee: fee to use on the close rate (optional).
|
||||||
:return: profit ratio as float
|
: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),
|
rate=(rate or self.close_rate),
|
||||||
fee=(fee or self.fee_close)
|
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}")
|
return float(f"{profit_ratio:.8f}")
|
||||||
|
|
||||||
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
|
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
|
||||||
|
@ -88,9 +88,6 @@ class StrategyResolver(IResolver):
|
|||||||
StrategyResolver._override_attribute_helper(strategy, config,
|
StrategyResolver._override_attribute_helper(strategy, config,
|
||||||
attribute, default)
|
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
|
# Loop this list again to have output combined
|
||||||
for attribute, _, subkey in attributes:
|
for attribute, _, subkey in attributes:
|
||||||
if subkey and attribute in config[subkey]:
|
if subkey and attribute in config[subkey]:
|
||||||
@ -98,11 +95,7 @@ class StrategyResolver(IResolver):
|
|||||||
elif attribute in config:
|
elif attribute in config:
|
||||||
logger.info("Strategy using %s: %s", attribute, config[attribute])
|
logger.info("Strategy using %s: %s", attribute, config[attribute])
|
||||||
|
|
||||||
# Sort and apply type conversions
|
StrategyResolver._normalize_attributes(strategy)
|
||||||
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._strategy_sanity_validations(strategy)
|
StrategyResolver._strategy_sanity_validations(strategy)
|
||||||
return strategy
|
return strategy
|
||||||
@ -131,6 +124,24 @@ class StrategyResolver(IResolver):
|
|||||||
setattr(strategy, attribute, default)
|
setattr(strategy, attribute, default)
|
||||||
config[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
|
@staticmethod
|
||||||
def _strategy_sanity_validations(strategy):
|
def _strategy_sanity_validations(strategy):
|
||||||
if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES):
|
if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES):
|
||||||
|
@ -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}/logs', 'log', view_func=self._get_logs, methods=['GET'])
|
||||||
self.app.add_url_rule(f'{BASE_URI}/profit', 'profit',
|
self.app.add_url_rule(f'{BASE_URI}/profit', 'profit',
|
||||||
view_func=self._profit, methods=['GET'])
|
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',
|
self.app.add_url_rule(f'{BASE_URI}/performance', 'performance',
|
||||||
view_func=self._performance, methods=['GET'])
|
view_func=self._performance, methods=['GET'])
|
||||||
self.app.add_url_rule(f'{BASE_URI}/status', 'status',
|
self.app.add_url_rule(f'{BASE_URI}/status', 'status',
|
||||||
@ -417,6 +419,18 @@ class ApiServer(RPC):
|
|||||||
|
|
||||||
return jsonify(stats)
|
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
|
@require_login
|
||||||
@rpc_catch_errors
|
@rpc_catch_errors
|
||||||
def _performance(self):
|
def _performance(self):
|
||||||
|
@ -275,6 +275,39 @@ class RPC:
|
|||||||
"trades_count": len(output)
|
"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(
|
def _rpc_trade_statistics(
|
||||||
self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||||
""" Returns cumulative profit statistics """
|
""" Returns cumulative profit statistics """
|
||||||
|
@ -5,6 +5,7 @@ This module manage Telegram communication
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Any, Callable, Dict, List, Union
|
from typing import Any, Callable, Dict, List, Union
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
@ -98,6 +99,7 @@ class Telegram(RPC):
|
|||||||
CommandHandler('trades', self._trades),
|
CommandHandler('trades', self._trades),
|
||||||
CommandHandler('delete', self._delete_trade),
|
CommandHandler('delete', self._delete_trade),
|
||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
|
CommandHandler('stats', self._stats),
|
||||||
CommandHandler('daily', self._daily),
|
CommandHandler('daily', self._daily),
|
||||||
CommandHandler('count', self._count),
|
CommandHandler('count', self._count),
|
||||||
CommandHandler('locks', self._locks),
|
CommandHandler('locks', self._locks),
|
||||||
@ -388,6 +390,48 @@ class Telegram(RPC):
|
|||||||
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
|
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
|
||||||
self._send_msg(markdown_msg)
|
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
|
@authorized_only
|
||||||
def _balance(self, update: Update, context: CallbackContext) -> None:
|
def _balance(self, update: Update, context: CallbackContext) -> None:
|
||||||
""" Handler for /balance """
|
""" Handler for /balance """
|
||||||
@ -743,6 +787,8 @@ class Telegram(RPC):
|
|||||||
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
||||||
"*/performance:* `Show performance of each finished trade grouped by pair`\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"
|
"*/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"
|
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
|
||||||
"*/locks:* `Show currently locked pairs`\n"
|
"*/locks:* `Show currently locked pairs`\n"
|
||||||
"*/balance:* `Show account balance per currency`\n"
|
"*/balance:* `Show account balance per currency`\n"
|
||||||
|
@ -551,8 +551,7 @@ class IStrategy(ABC):
|
|||||||
# evaluate if the stoploss was hit if stoploss is not on exchange
|
# 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
|
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
||||||
# regular stoploss handling.
|
# regular stoploss handling.
|
||||||
if ((self.stoploss is not None) and
|
if ((trade.stop_loss >= current_rate) and
|
||||||
(trade.stop_loss >= current_rate) and
|
|
||||||
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
|
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
|
||||||
|
|
||||||
sell_type = SellType.STOP_LOSS
|
sell_type = SellType.STOP_LOSS
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
numpy==1.19.4
|
numpy==1.19.4
|
||||||
pandas==1.1.4
|
pandas==1.1.4
|
||||||
|
|
||||||
ccxt==1.38.55
|
ccxt==1.39.10
|
||||||
aiohttp==3.7.3
|
aiohttp==3.7.3
|
||||||
SQLAlchemy==1.3.20
|
SQLAlchemy==1.3.20
|
||||||
python-telegram-bot==13.1
|
python-telegram-bot==13.1
|
||||||
|
@ -139,6 +139,13 @@ class FtRestClient():
|
|||||||
"""
|
"""
|
||||||
return self._get("profit")
|
return self._get("profit")
|
||||||
|
|
||||||
|
def stats(self):
|
||||||
|
"""Return the stats report (durations, sell-reasons).
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("stats")
|
||||||
|
|
||||||
def performance(self):
|
def performance(self):
|
||||||
"""Return the performance of the different coins.
|
"""Return the performance of the different coins.
|
||||||
|
|
||||||
|
@ -1588,16 +1588,7 @@ def fetch_trades_result():
|
|||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def trades_for_order2():
|
def trades_for_order2():
|
||||||
return [{'info': {'id': 34567,
|
return [{'info': {},
|
||||||
'orderId': 123456,
|
|
||||||
'price': '0.24544100',
|
|
||||||
'qty': '8.00000000',
|
|
||||||
'commission': '0.00800000',
|
|
||||||
'commissionAsset': 'LTC',
|
|
||||||
'time': 1521663363189,
|
|
||||||
'isBuyer': True,
|
|
||||||
'isMaker': False,
|
|
||||||
'isBestMatch': True},
|
|
||||||
'timestamp': 1521663363189,
|
'timestamp': 1521663363189,
|
||||||
'datetime': '2018-03-21T20:16:03.189Z',
|
'datetime': '2018-03-21T20:16:03.189Z',
|
||||||
'symbol': 'LTC/ETH',
|
'symbol': 'LTC/ETH',
|
||||||
@ -1609,16 +1600,7 @@ def trades_for_order2():
|
|||||||
'cost': 1.963528,
|
'cost': 1.963528,
|
||||||
'amount': 4.0,
|
'amount': 4.0,
|
||||||
'fee': {'cost': 0.004, 'currency': 'LTC'}},
|
'fee': {'cost': 0.004, 'currency': 'LTC'}},
|
||||||
{'info': {'id': 34567,
|
{'info': {},
|
||||||
'orderId': 123456,
|
|
||||||
'price': '0.24544100',
|
|
||||||
'qty': '8.00000000',
|
|
||||||
'commission': '0.00800000',
|
|
||||||
'commissionAsset': 'LTC',
|
|
||||||
'time': 1521663363189,
|
|
||||||
'isBuyer': True,
|
|
||||||
'isMaker': False,
|
|
||||||
'isBestMatch': True},
|
|
||||||
'timestamp': 1521663363189,
|
'timestamp': 1521663363189,
|
||||||
'datetime': '2018-03-21T20:16:03.189Z',
|
'datetime': '2018-03-21T20:16:03.189Z',
|
||||||
'symbol': 'LTC/ETH',
|
'symbol': 'LTC/ETH',
|
||||||
@ -1632,6 +1614,14 @@ def trades_for_order2():
|
|||||||
'fee': {'cost': 0.004, 'currency': 'LTC'}}]
|
'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
|
@pytest.fixture
|
||||||
def buy_order_fee():
|
def buy_order_fee():
|
||||||
return {
|
return {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from freqtrade.persistence.models import Order, Trade
|
from freqtrade.persistence.models import Order, Trade
|
||||||
|
|
||||||
|
|
||||||
@ -82,6 +84,9 @@ def mock_trade_2(fee):
|
|||||||
is_open=False,
|
is_open=False,
|
||||||
open_order_id='dry_run_sell_12345',
|
open_order_id='dry_run_sell_12345',
|
||||||
strategy='DefaultStrategy',
|
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')
|
o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy')
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
@ -134,6 +139,9 @@ def mock_trade_3(fee):
|
|||||||
close_profit=0.01,
|
close_profit=0.01,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
is_open=False,
|
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')
|
o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy')
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
|
@ -62,7 +62,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'fee_close_cost': ANY,
|
'fee_close_cost': ANY,
|
||||||
'fee_close_currency': ANY,
|
'fee_close_currency': ANY,
|
||||||
'open_rate_requested': ANY,
|
'open_rate_requested': ANY,
|
||||||
'open_trade_price': 0.0010025,
|
'open_trade_value': 0.0010025,
|
||||||
'close_rate_requested': ANY,
|
'close_rate_requested': ANY,
|
||||||
'sell_reason': ANY,
|
'sell_reason': ANY,
|
||||||
'sell_order_status': 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_cost': ANY,
|
||||||
'fee_close_currency': ANY,
|
'fee_close_currency': ANY,
|
||||||
'open_rate_requested': ANY,
|
'open_rate_requested': ANY,
|
||||||
'open_trade_price': ANY,
|
'open_trade_value': ANY,
|
||||||
'close_rate_requested': ANY,
|
'close_rate_requested': ANY,
|
||||||
'sell_reason': ANY,
|
'sell_reason': ANY,
|
||||||
'sell_order_status': ANY,
|
'sell_order_status': ANY,
|
||||||
|
@ -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):
|
def test_api_performance(botclient, mocker, ticker, fee):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
patch_get_signal(ftbot, (True, False))
|
patch_get_signal(ftbot, (True, False))
|
||||||
@ -678,7 +707,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'min_rate': 1.098e-05,
|
'min_rate': 1.098e-05,
|
||||||
'open_order_id': None,
|
'open_order_id': None,
|
||||||
'open_rate_requested': 1.098e-05,
|
'open_rate_requested': 1.098e-05,
|
||||||
'open_trade_price': 0.0010025,
|
'open_trade_value': 0.0010025,
|
||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
@ -805,7 +834,7 @@ def test_api_forcebuy(botclient, mocker, fee):
|
|||||||
'min_rate': None,
|
'min_rate': None,
|
||||||
'open_order_id': '123456',
|
'open_order_id': '123456',
|
||||||
'open_rate_requested': None,
|
'open_rate_requested': None,
|
||||||
'open_trade_price': 0.24605460,
|
'open_trade_value': 0.24605460,
|
||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
|
@ -74,9 +74,10 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
|
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
|
||||||
"['delete'], ['performance'], ['daily'], ['count'], ['locks'], "
|
"['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], "
|
||||||
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], "
|
"['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)
|
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]
|
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:
|
def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None:
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
||||||
|
@ -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).',
|
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
|
||||||
caplog)
|
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,
|
def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, fee,
|
||||||
caplog, mocker):
|
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)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
def patch_with_fee(order):
|
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]}})
|
'currency': order['symbol'].split('/')[0]}})
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
@ -177,10 +177,10 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
|
|||||||
|
|
||||||
trade.open_order_id = 'something'
|
trade.open_order_id = 'something'
|
||||||
trade.update(limit_buy_order)
|
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)
|
trade.update(limit_sell_order)
|
||||||
assert trade.calc_close_trade_price() == 0.0010646656050132426
|
assert trade.calc_close_trade_value() == 0.0010646656050132426
|
||||||
|
|
||||||
# Profit in BTC
|
# Profit in BTC
|
||||||
assert trade.calc_profit() == 0.00006217
|
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.open_order_id = 'something'
|
||||||
trade.update(limit_buy_order)
|
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")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@ -277,7 +277,7 @@ def test_update_invalid_order(limit_buy_order):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@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(
|
trade = Trade(
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
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
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
# Get the open rate price with the standard fee rate
|
# 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
|
trade.fee_open = 0.003
|
||||||
# Get the open rate price with a custom fee rate
|
# 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")
|
@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
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
# Get the close rate price with a custom close rate and a regular fee rate
|
# 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
|
# 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
|
# Test when we apply a Sell order, and ask price with a custom fee rate
|
||||||
trade.update(limit_sell_order)
|
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")
|
@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.max_rate == 0.0
|
||||||
assert trade.stop_loss == 0.0
|
assert trade.stop_loss == 0.0
|
||||||
assert trade.initial_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.close_profit_abs is None
|
||||||
assert trade.fee_open_cost is None
|
assert trade.fee_open_cost is None
|
||||||
assert trade.fee_open_currency 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_bak1", caplog)
|
||||||
assert log_has("trying trades_bak2", caplog)
|
assert log_has("trying trades_bak2", caplog)
|
||||||
assert log_has("Running database migration for trades - backup: 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 trade.close_profit_abs is None
|
||||||
|
|
||||||
assert log_has("Moving open orders to Orders table.", caplog)
|
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.max_rate == 0.0
|
||||||
assert trade.stop_loss == 0.0
|
assert trade.stop_loss == 0.0
|
||||||
assert trade.initial_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("trying trades_bak0", caplog)
|
||||||
assert log_has("Running database migration for trades - backup: 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,
|
'close_timestamp': None,
|
||||||
'open_rate': 0.123,
|
'open_rate': 0.123,
|
||||||
'open_rate_requested': None,
|
'open_rate_requested': None,
|
||||||
'open_trade_price': 15.1668225,
|
'open_trade_value': 15.1668225,
|
||||||
'fee_close': 0.0025,
|
'fee_close': 0.0025,
|
||||||
'fee_close_cost': None,
|
'fee_close_cost': None,
|
||||||
'fee_close_currency': None,
|
'fee_close_currency': None,
|
||||||
@ -896,7 +896,7 @@ def test_to_json(default_conf, fee):
|
|||||||
'min_rate': None,
|
'min_rate': None,
|
||||||
'open_order_id': None,
|
'open_order_id': None,
|
||||||
'open_rate_requested': None,
|
'open_rate_requested': None,
|
||||||
'open_trade_price': 12.33075,
|
'open_trade_value': 12.33075,
|
||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
|
Loading…
Reference in New Issue
Block a user