Merge branch 'develop' into feat/short

This commit is contained in:
Matthias 2021-11-18 20:20:01 +01:00
commit f40221dd9f
49 changed files with 1112 additions and 363 deletions

View File

@ -56,6 +56,13 @@ To help with that, we encourage you to install the git pre-commit
hook that will warn you when you try to commit code that fails these checks.
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
##### Additional styles applied
* Have docstrings on all public methods
* Use double-quotes for docstrings
* Multiline docstrings should be indented to the level of the first quote
* Doc-strings should follow the reST format (`:param xxx: ...`, `:return: ...`, `:raises KeyError: ... `)
### 3. Test if all type-hints are correct
#### Run mypy

View File

@ -28,6 +28,7 @@
"unfilledtimeout": {
"buy": 10,
"sell": 30,
"exit_timeout_count": 0,
"unit": "minutes"
},
"bid_strategy": {

View File

@ -104,6 +104,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
@ -215,7 +216,7 @@ With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)
To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit.
!!! Warning
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange.
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. Freqtrade adjusts the stake-amount to this value, unless it's > 30% more than the calculated/desired stake-amount - in which case the trade is rejected.
#### Tradable balance

View File

@ -26,6 +26,8 @@ Alternatively (e.g. if your system is not supported by the setup.sh script), fol
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
Before opening a pull request, please familiarize yourself with our [Contributing Guidelines](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md).
### Devcontainer setup
The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension.

View File

@ -198,7 +198,7 @@ Not defining this parameter (or setting it to 0) will use all-time performance.
The optional `min_profit` parameter defines the minimum profit a pair must have to be considered.
Pairs below this level will be filtered out.
Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without without a way to recover.
Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without a way to recover.
```json
"pairlists": [
@ -211,6 +211,8 @@ Using this parameter without `minutes` is highly discouraged, as it can lead to
],
```
As this Filter uses past performance of the bot, it'll have some startup-period - and should only be used after the bot has a few 100 trades in the database.
!!! Warning "Backtesting"
`PerformanceFilter` does not support backtesting mode.

View File

@ -36,7 +36,7 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#binance-blacklist))
- [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com)
- [X] [Gate.io](https://www.gate.io/ref/6266643)

View File

@ -60,7 +60,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
sudo apt-get update
# install packages
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git curl
```
=== "RaspberryPi/Raspbian"
@ -71,7 +71,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
```bash
sudo apt-get install python3-venv libatlas-base-dev cmake
sudo apt-get install python3-venv libatlas-base-dev cmake curl
# 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,4 +1,4 @@
mkdocs==1.2.3
mkdocs-material==7.3.6
mdx_truly_sane_lists==1.2
pymdown-extensions==9.0
pymdown-extensions==9.1

View File

@ -143,6 +143,52 @@ def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_r
!!! Note
`buy_tag` is limited to 100 characters, remaining data will be truncated.
## Exit tag
Similar to [Buy Tagging](#buy-tag), you can also specify a sell tag.
``` python
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe['rsi'] > 70) &
(dataframe['volume'] > 0)
),
['sell', 'exit_tag']] = (1, 'exit_rsi')
return dataframe
```
The provided exit-tag is then used as sell-reason - and shown as such in backtest results.
!!! Note
`sell_reason` is limited to 100 characters, remaining data will be truncated.
## Bot loop start callback
A simple callback which is called once at the start of every bot throttling iteration.
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
``` python
import requests
class AwesomeStrategy(IStrategy):
# ... populate_* methods
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
if self.config['runmode'].value in ('live', 'dry_run'):
# Assign this to the class by using self.*
# can then be used by populate_* methods
self.remote_data = requests.get('https://some_remote_source.example.com')
```
## Custom stoploss
@ -501,32 +547,6 @@ class AwesomeStrategy(IStrategy):
---
## Bot loop start callback
A simple callback which is called once at the start of every bot throttling iteration.
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
``` python
import requests
class AwesomeStrategy(IStrategy):
# ... populate_* methods
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
if self.config['runmode'].value in ('live', 'dry_run'):
# Assign this to the class by using self.*
# can then be used by populate_* methods
self.remote_data = requests.get('https://some_remote_source.example.com')
```
## Bot order confirmation
### Trade entry (buy order) confirmation

View File

@ -4,33 +4,23 @@ This page explains how to customize your strategies, add new indicators and set
Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates.
## Install a custom strategy file
This is very simple. Copy paste your strategy file into the directory `user_data/strategies`.
Let assume you have a class called `AwesomeStrategy` in the file `AwesomeStrategy.py`:
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/AwesomeStrategy.py`
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
```bash
freqtrade trade --strategy AwesomeStrategy
```
## Develop your own strategy
The bot includes a default strategy file.
Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
You will however most likely have your own idea for a strategy.
This document intends to help you develop one for yourself.
This document intends to help you convert your strategy idea into your own strategy.
To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`.
To get started, use `freqtrade new-strategy --strategy AwesomeStrategy` (you can obviously use your own naming for your strategy).
This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`.
!!! Note
This is just a template file, which will most likely not be profitable out of the box.
??? Hint "Different template levels"
`freqtrade new-strategy` has an additional parameter, `--template`, which controls the amount of pre-build information you get in the created strategy. Use `--template minimal` to get an empty strategy without any indicator examples, or `--template advanced` to get a template with most callbacks defined.
### Anatomy of a strategy
A strategy file contains all the information needed to build a good strategy:
@ -67,6 +57,46 @@ file as reference.**
needs to take care to avoid having the strategy utilize data from the future.
Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
### Dataframe
Freqtrade uses [pandas](https://pandas.pydata.org/) to store/provide the candlestick (OHLCV) data.
Pandas is a great library developed for processing large amounts of data.
Each row in a dataframe corresponds to one candle on a chart, with the latest candle always being the last in the dataframe (sorted by date).
``` output
> dataframe.head()
date open high low close volume
0 2021-11-09 23:25:00+00:00 67279.67 67321.84 67255.01 67300.97 44.62253
1 2021-11-09 23:30:00+00:00 67300.97 67301.34 67183.03 67187.01 61.38076
2 2021-11-09 23:35:00+00:00 67187.02 67187.02 67031.93 67123.81 113.42728
3 2021-11-09 23:40:00+00:00 67123.80 67222.40 67080.33 67160.48 78.96008
4 2021-11-09 23:45:00+00:00 67160.48 67160.48 66901.26 66943.37 111.39292
```
Pandas provides fast ways to calculate metrics. To benefit from this speed, it's advised to not use loops, but use vectorized methods instead.
Vectorized operations perform calculations across the whole range of data and are therefore, compared to looping through each row, a lot faster when calculating indicators.
As a dataframe is a table, simple python comparisons like the following will not work
``` python
if dataframe['rsi'] > 30:
dataframe['buy'] = 1
```
The above section will fail with `The truth value of a Series is ambiguous. [...]`.
This must instead be written in a pandas-compatible way, so the operation is performed across the whole dataframe.
``` python
dataframe.loc[
(dataframe['rsi'] > 30)
, 'buy'] = 1
```
With this section, you have a new column in your dataframe, which has `1` assigned whenever RSI is above 30.
### Customize Indicators
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
@ -134,7 +164,7 @@ Additional technical libraries can be installed as necessary, or custom indicato
### Strategy startup period
Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
Most indicators have an instable startup period, in which they are either not available (NaN), or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators.
@ -146,8 +176,14 @@ In this example strategy, this should be set to 100 (`startup_candle_count = 100
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
!!! Warning "Using x calls to get OHLCV"
If you receive a warning like `WARNING - Using 3 calls to get OHLCV. This can result in slower operations for the bot. Please check if you really need 1500 candles for your strategy` - you should consider if you really need this much historic data for your signals.
Having this will cause Freqtrade to make multiple calls for the same pair, which will obviously be slower than one network request.
As a consequence, Freqtrade will take longer to refresh candles - and should therefore be avoided if possible.
This is capped to 5 total calls to avoid overloading the exchange, or make freqtrade too slow.
!!! Warning
`startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations.
`startup_candle_count` should be below `ohlcv_candle_limit * 5` (which is 500 * 5 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations.
#### Example
@ -312,6 +348,19 @@ Currently this is `pair`, which can be accessed using `metadata['pair']` - and w
The Metadata-dict should not be modified and does not persist information across multiple calls.
Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information)
## Strategy file loading
By default, freqtrade will attempt to load strategies from all `.py` files within `user_data/strategies`.
Assuming your strategy is called `AwesomeStrategy`, stored in the file `user_data/strategies/AwesomeStrategy.py`, then you can start freqtrade with `freqtrade trade --strategy AwesomeStrategy`.
Note that we're using the class-name, not the file name.
You can use `freqtrade list-strategies` to see a list of all strategies Freqtrade is able to load (all strategies in the correct folder).
It will also include a "status" field, highlighting potential problems.
??? Hint "Customize strategy directory"
You can use a different directory by using `--strategy-path user_data/otherPath`. This parameter is available to all commands that require a strategy.
## Informative Pairs
### Get data for non-tradeable pairs

View File

@ -58,6 +58,8 @@ For the Freqtrade configuration, you can then use the the full value (including
```json
"chat_id": "-1001332619709"
```
!!! Warning "Using telegram groups"
When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasent surprises.
## Control telegram noise
@ -175,6 +177,8 @@ 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)
| `/weekly <n>` | Shows profit or loss per week, over the last n weeks (n defaults to 8)
| `/monthly <n>` | Shows profit or loss per month, over the last n months (n defaults to 6)
| `/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.
@ -307,8 +311,7 @@ Return the balance of all crypto-currency your have on the exchange.
### /daily <n>
Per default `/daily` will return the 7 last days.
The example below if for `/daily 3`:
Per default `/daily` will return the 7 last days. The example below if for `/daily 3`:
> **Daily Profit over the last 3 days:**
```
@ -319,6 +322,34 @@ Day Profit BTC Profit USD
2018-01-01 0.00269130 BTC 34.986 USD
```
### /weekly <n>
Per default `/weekly` will return the 8 last weeks, including the current week. Each week starts
from Monday. The example below if for `/weekly 3`:
> **Weekly Profit over the last 3 weeks (starting from Monday):**
```
Monday Profit BTC Profit USD
---------- -------------- ------------
2018-01-03 0.00224175 BTC 29,142 USD
2017-12-27 0.00033131 BTC 4,307 USD
2017-12-20 0.00269130 BTC 34.986 USD
```
### /monthly <n>
Per default `/monthly` will return the 6 last months, including the current month. The example below
if for `/monthly 3`:
> **Monthly Profit over the last 3 months:**
```
Month Profit BTC Profit USD
---------- -------------- ------------
2018-01 0.00224175 BTC 29,142 USD
2017-12 0.00033131 BTC 4,307 USD
2017-11 0.00269130 BTC 34.986 USD
```
### /whitelist
Shows the current whitelist

View File

@ -160,6 +160,7 @@ CONF_SCHEMA = {
'properties': {
'buy': {'type': 'number', 'minimum': 1},
'sell': {'type': 'number', 'minimum': 1},
'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0},
'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'}
}
},
@ -210,7 +211,10 @@ CONF_SCHEMA = {
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'emergencysell': {
'type': 'string',
'enum': ORDERTYPE_POSSIBILITIES,
'default': 'market'},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'},
'stoploss_on_exchange_interval': {'type': 'number'},

View File

@ -113,7 +113,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
if len_before != len_after:
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}"
f" - {round(pct_missing * 100, 2)}%")
f" - {pct_missing:.2%}")
if pct_missing > 0.01:
logger.info(message)
else:

View File

@ -1,5 +1,3 @@
class FreqtradeException(Exception):
"""
Freqtrade base exception. Handled at the outermost level.

View File

@ -201,8 +201,9 @@ class Binance(Exchange):
raise OperationalException(e) from e
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, is_new_pair: bool
) -> List:
since_ms: int, is_new_pair: bool = False,
raise_: bool = False
) -> Tuple[str, str, List]:
"""
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
Does not work for other exchanges, which don't return the earliest data when called with "0"
@ -215,7 +216,8 @@ class Binance(Exchange):
logger.info(f"Candle-data for {pair} available starting with "
f"{arrow.get(since_ms // 1000).isoformat()}.")
return await super()._async_get_historic_ohlcv(
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair,
raise_=raise_)
def funding_fee_cutoff(self, open_date: datetime):
"""

View File

@ -81,9 +81,16 @@ def retrier_async(f):
count -= 1
kwargs.update({'count': count})
if isinstance(ex, DDosProtection):
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
await asyncio.sleep(backoff_delay)
if "kucoin" in str(ex) and "429000" in str(ex):
# Temporary fix for 429000 error on kucoin
# see https://github.com/freqtrade/freqtrade/issues/5700 for details.
logger.warning(
f"Kucoin 429 error, avoid triggering DDosProtection backoff delay. "
f"{count} tries left before giving up")
else:
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
await asyncio.sleep(backoff_delay)
return await wrapper(*args, **kwargs)
else:
logger.warning('Giving up retrying: %s()', f.__name__)

View File

@ -168,9 +168,10 @@ class Exchange:
self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.validate_required_startup_candles(config.get('startup_candle_count', 0),
config.get('timeframe', ''))
self.required_candle_call_count = self.validate_required_startup_candles(
config.get('startup_candle_count', 0), config.get('timeframe', ''))
self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral)
# Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_config.get(
"markets_refresh_interval", 60) * 60
@ -523,16 +524,29 @@ class Exchange:
raise OperationalException(
f'Time in force policies are not supported for {self.name} yet.')
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> None:
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
"""
Checks if required startup_candles is more than ohlcv_candle_limit().
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
"""
candle_limit = self.ohlcv_candle_limit(timeframe)
if startup_candles + 5 > candle_limit:
# Require one more candle - to account for the still open candle.
candle_count = startup_candles + 1
# Allow 5 calls to the exchange per pair
required_candle_call_count = int(
(candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
if required_candle_call_count > 5:
# Only allow 5 calls per pair to somewhat limit the impact
raise OperationalException(
f"This strategy requires {startup_candles} candles to start. "
f"{self.name} only provides {candle_limit - 5} for {timeframe}.")
f"This strategy requires {startup_candles} candles to start, which is more than 5x "
f"the amount of candles {self.name} provides for {timeframe}.")
if required_candle_call_count > 1:
logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
f"This can result in slower operations for the bot. Please check "
f"if you really need {startup_candles} candles for your strategy")
return required_candle_call_count
def validate_trading_mode_and_collateral(
self,
@ -1306,9 +1320,11 @@ class Exchange:
:param since_ms: Timestamp in milliseconds to get history from
:return: List with candle (OHLCV) data
"""
return asyncio.get_event_loop().run_until_complete(
pair, timeframe, data = asyncio.get_event_loop().run_until_complete(
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
since_ms=since_ms, is_new_pair=is_new_pair))
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
return data
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
since_ms: int) -> DataFrame:
@ -1324,8 +1340,9 @@ class Exchange:
drop_incomplete=self._ohlcv_partial_candle)
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, is_new_pair: bool
) -> List:
since_ms: int, is_new_pair: bool = False,
raise_: bool = False
) -> Tuple[str, str, List]:
"""
Download historic ohlcv
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
@ -1349,15 +1366,17 @@ class Exchange:
for res in results:
if isinstance(res, Exception):
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
if raise_:
raise
continue
# Deconstruct tuple if it's not an exception
p, _, new_data = res
if p == pair:
data.extend(new_data)
else:
# Deconstruct tuple if it's not an exception
p, _, new_data = res
if p == pair:
data.extend(new_data)
# Sort data again after extending the result - above calls return in "async order"
data = sorted(data, key=lambda x: x[0])
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
return data
return pair, timeframe, data
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
since_ms: Optional[int] = None, cache: bool = True
@ -1377,10 +1396,22 @@ class Exchange:
cached_pairs = []
# Gather coroutines to run
for pair, timeframe in set(pair_list):
if (((pair, timeframe) not in self._klines)
if ((pair, timeframe) not in self._klines
or self._now_is_time_to_refresh(pair, timeframe)):
input_coroutines.append(self._async_get_candle_history(pair, timeframe,
since_ms=since_ms))
if not since_ms and self.required_candle_call_count > 1:
# Multiple calls for one pair - to get more history
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
move_to = one_call * self.required_candle_call_count
now = timeframe_to_next_date(timeframe)
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
if since_ms:
input_coroutines.append(self._async_get_historic_ohlcv(
pair, timeframe, since_ms=since_ms, raise_=True))
else:
# One call ... "regular" refresh
input_coroutines.append(self._async_get_candle_history(
pair, timeframe, since_ms=since_ms))
else:
logger.debug(
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",

View File

@ -1,4 +1,4 @@
""" Kucoin exchange subclass """
"""Kucoin exchange subclass."""
import logging
from typing import Dict
@ -9,9 +9,9 @@ logger = logging.getLogger(__name__)
class Kucoin(Exchange):
"""
Kucoin exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
"""Kucoin exchange class.
Contains adjustments needed for Freqtrade to work with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features

View File

@ -9,9 +9,9 @@ logger = logging.getLogger(__name__)
class Okex(Exchange):
"""
Okex exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
"""Okex exchange class.
Contains adjustments needed for Freqtrade to work with this exchange.
"""
_ft_has: Dict = {

View File

@ -219,19 +219,20 @@ class FreqtradeBot(LoggingMixin):
def check_for_open_trades(self):
"""
Notify the user when the bot is stopped
Notify the user when the bot is stopped (not reloaded)
and there are still open trades active.
"""
open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
if len(open_trades) != 0:
if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
msg = {
'type': RPCMessageType.WARNING,
'status': f"{len(open_trades)} open trades active.\n\n"
f"Handle these trades manually on {self.exchange.name}, "
f"or '/start' the bot again and use '/stopbuy' "
f"to handle open trades gracefully. \n"
f"{'Trades are simulated.' if self.config['dry_run'] else ''}",
'status':
f"{len(open_trades)} open trades active.\n\n"
f"Handle these trades manually on {self.exchange.name}, "
f"or '/start' the bot again and use '/stopbuy' "
f"to handle open trades gracefully. \n"
f"{'Note: Trades are simulated (dry run).' if self.config['dry_run'] else ''}",
}
self.rpc.send_msg(msg)
@ -622,7 +623,7 @@ class FreqtradeBot(LoggingMixin):
side='short' if is_short else 'long'
)
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount:
return False
@ -1075,6 +1076,7 @@ class FreqtradeBot(LoggingMixin):
side = trade.enter_side if is_entering else trade.exit_side
timed_out = self._check_timed_out(side, order)
time_method = 'check_sell_timeout' if order['side'] == 'sell' else 'check_buy_timeout'
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
if not_closed and (fully_cancelled or timed_out or (
strategy_safe_wrapper(getattr(self.strategy, time_method), default_retval=False)(
@ -1087,6 +1089,13 @@ class FreqtradeBot(LoggingMixin):
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
else:
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
canceled_count = trade.get_exit_order_count()
if max_timeouts > 0 and canceled_count >= max_timeouts:
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
f'timed out {max_timeouts} times.')
self.execute_trade_exit(
trade, order.get('price'),
sell_reason=SellCheckTuple(sell_type=SellType.EMERGENCY_SELL))
def cancel_all_open_orders(self) -> None:
"""
@ -1468,7 +1477,7 @@ class FreqtradeBot(LoggingMixin):
if self.exchange.check_order_canceled_empty(order):
# Trade has been cancelled on exchange
# Handling of this will happen in check_handle_timeout.
# Handling of this will happen in check_handle_timedout.
return True
# Try update amount (binance-fix)
@ -1559,14 +1568,17 @@ class FreqtradeBot(LoggingMixin):
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, order.get('trades', []))
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,
trades: List) -> float:
"""
fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
fee-detection fallback to Trades.
Either uses provided trades list or the result of fetch_my_trades to get correct fee.
"""
trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order),
trade.pair, trade.open_date)
if not trades:
trades = self.exchange.get_trades_for_order(
self.exchange.get_order_id_conditional(order), trade.pair, trade.open_date)
if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)

View File

@ -379,16 +379,6 @@ class Backtesting:
if sell.sell_flag:
trade.close_date = sell_candle_time
trade.sell_reason = sell.sell_reason
# Checks and adds an exit tag, after checking that the length of the
# sell_row has the length for an exit tag column
if(
len(sell_row) > EXIT_TAG_IDX
and sell_row[EXIT_TAG_IDX] is not None
and len(sell_row[EXIT_TAG_IDX]) > 0
):
trade.sell_reason = sell_row[EXIT_TAG_IDX]
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
@ -403,6 +393,17 @@ class Backtesting:
current_time=sell_candle_time):
return None
trade.sell_reason = sell.sell_reason
# Checks and adds an exit tag, after checking that the length of the
# sell_row has the length for an exit tag column
if(
len(sell_row) > EXIT_TAG_IDX
and sell_row[EXIT_TAG_IDX] is not None
and len(sell_row[EXIT_TAG_IDX]) > 0
):
trade.sell_reason = sell_row[EXIT_TAG_IDX]
trade.close(closerate, show_msg=False)
return trade
@ -451,7 +452,7 @@ class Backtesting:
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount,
side=direction)
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount:
return None

View File

@ -284,10 +284,10 @@ class HyperoptTools():
return (f"{results_metrics['total_trades']:6d} trades. "
f"{results_metrics['wins']}/{results_metrics['draws']}"
f"/{results_metrics['losses']} Wins/Draws/Losses. "
f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. "
f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. "
f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} "
f"({results_metrics['profit_total'] * 100: 7.2f}%). "
f"Avg profit {results_metrics['profit_mean']:7.2%}. "
f"Median profit {results_metrics['profit_median']:7.2%}. "
f"Total profit {results_metrics['profit_total_abs']:11.8f} {stake_currency} "
f"({results_metrics['profit_total']:8.2%}). "
f"Avg duration {results_metrics['holding_avg']} min."
)

View File

@ -725,22 +725,22 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])),
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
strat_results['stake_currency'])),
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"),
('Total profit %', f"{strat_results['profit_total']:.2%}"),
('Trades per day', strat_results['trades_per_day']),
('Avg. daily profit %',
f"{round(strat_results['profit_total'] / strat_results['backtest_days'] * 100, 2)}%"),
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
strat_results['stake_currency'])),
('Total trade volume', round_coin_value(strat_results['total_volume'],
strat_results['stake_currency'])),
('', ''), # Empty line to improve readability
('Best Pair', f"{strat_results['best_pair']['key']} "
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
f"{strat_results['best_pair']['profit_sum']:.2%}"),
('Worst Pair', f"{strat_results['worst_pair']['key']} "
f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"),
('Best trade', f"{best_trade['pair']} {round(best_trade['profit_ratio'] * 100, 2)}%"),
f"{strat_results['worst_pair']['profit_sum']:.2%}"),
('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"),
('Worst trade', f"{worst_trade['pair']} "
f"{round(worst_trade['profit_ratio'] * 100, 2)}%"),
f"{worst_trade['profit_ratio']:.2%}"),
('Best day', round_coin_value(strat_results['backtest_best_day_abs'],
strat_results['stake_currency'])),
@ -758,7 +758,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Max balance', round_coin_value(strat_results['csum_max'],
strat_results['stake_currency'])),
('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
('Drawdown', f"{strat_results['max_drawdown']:.2%}"),
('Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
strat_results['stake_currency'])),
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
@ -767,7 +767,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])),
('Drawdown Start', strat_results['drawdown_start']),
('Drawdown End', strat_results['drawdown_end']),
('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
('Market change', f"{strat_results['market_change']:.2%}"),
]
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
@ -864,5 +864,5 @@ def show_sorted_pairlist(config: Dict, backtest_stats: Dict):
print(f"Pairs for Strategy {strategy}: \n[")
for result in results['results_per_pair']:
if result["key"] != 'TOTAL':
print(f'"{result["key"]}", // {round(result["profit_mean_pct"], 2)}%')
print(f'"{result["key"]}", // {result["profit_mean"]:.2%}')
print("]")

View File

@ -200,6 +200,8 @@ class Order(_DECL_BASE):
@staticmethod
def get_open_orders() -> List['Order']:
"""
Retrieve open orders from the database
:return: List of open orders
"""
return Order.query.filter(Order.ft_is_open.is_(True)).all()
@ -620,6 +622,13 @@ class LocalTrade():
def update_order(self, order: Dict) -> None:
Order.update_orders(self.orders, order)
def get_exit_order_count(self) -> int:
"""
Get amount of failed exiting orders
assumes full exits.
"""
return len([o for o in self.orders if o.ft_order_side == 'sell'])
def _calc_open_trade_value(self) -> float:
"""
Calculate the open_rate including open_fee.
@ -1002,7 +1011,7 @@ class Trade(_DECL_BASE, LocalTrade):
return Trade.query
@staticmethod
def get_open_order_trades():
def get_open_order_trades() -> List['Trade']:
"""
Returns all open trades
NOTE: Not supported in Backtesting.
@ -1190,6 +1199,7 @@ class Trade(_DECL_BASE, LocalTrade):
if not any(item["mix_tag"] == mix_tag for item in return_list):
return_list.append({'mix_tag': mix_tag,
'profit': profit,
'profit_pct': round(profit * 100, 2),
'profit_abs': profit_abs,
'count': count})
else:
@ -1198,11 +1208,11 @@ class Trade(_DECL_BASE, LocalTrade):
return_list[i] = {
'mix_tag': mix_tag,
'profit': profit + return_list[i]["profit"],
'profit_pct': round(profit + return_list[i]["profit"] * 100, 2),
'profit_abs': profit_abs + return_list[i]["profit_abs"],
'count': 1 + return_list[i]["count"]}
i += 1
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in return_list]
return return_list
@staticmethod

View File

@ -169,8 +169,8 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'],
],
mode='markers',
name=f"Max drawdown {max_drawdown * 100:.2f}%",
text=f"Max drawdown {max_drawdown * 100:.2f}%",
name=f"Max drawdown {max_drawdown:.2%}",
text=f"Max drawdown {max_drawdown:.2%}",
marker=dict(
symbol='square-open',
size=9,
@ -192,7 +192,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
# Trades can be empty
if trades is not None and len(trades) > 0:
# Create description for sell summarizing the trade
trades['desc'] = trades.apply(lambda row: f"{round(row['profit_ratio'] * 100, 1)}%, "
trades['desc'] = trades.apply(lambda row: f"{row['profit_ratio']:.2%}, "
f"{row['sell_reason']}, "
f"{row['trade_duration']} min",
axis=1)

View File

@ -50,7 +50,7 @@ class PriceFilter(IPairList):
"""
active_price_filters = []
if self._low_price_ratio != 0:
active_price_filters.append(f"below {self._low_price_ratio * 100}%")
active_price_filters.append(f"below {self._low_price_ratio:.1%}")
if self._min_price != 0:
active_price_filters.append(f"below {self._min_price:.8f}")
if self._max_price != 0:
@ -82,7 +82,7 @@ class PriceFilter(IPairList):
changeperc = compare / ticker['last']
if changeperc > self._low_price_ratio:
self.log_once(f"Removed {pair} from whitelist, "
f"because 1 unit is {changeperc * 100:.3f}%", logger.info)
f"because 1 unit is {changeperc:.3%}", logger.info)
return False
# Perform low_amount check

View File

@ -34,7 +34,7 @@ class SpreadFilter(IPairList):
Short whitelist method description - used for startup-messages
"""
return (f"{self.name} - Filtering pairs with ask/bid diff above "
f"{self._max_spread_ratio * 100}%.")
f"{self._max_spread_ratio:.2%}.")
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
"""
@ -47,7 +47,7 @@ class SpreadFilter(IPairList):
spread = 1 - ticker['bid'] / ticker['ask']
if spread > self._max_spread_ratio:
self.log_once(f"Removed {pair} from whitelist, because spread "
f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%",
f"{spread * 100:.3%} > {self._max_spread_ratio:.3%}",
logger.info)
return False
else:

View File

@ -95,6 +95,7 @@ class Profit(BaseModel):
avg_duration: str
best_pair: str
best_rate: float
best_pair_profit_ratio: float
winning_trades: int
losing_trades: int
@ -123,7 +124,26 @@ class Daily(BaseModel):
stake_currency: str
class UnfilledTimeout(BaseModel):
buy: int
sell: int
unit: str
exit_timeout_count: Optional[int]
class OrderTypes(BaseModel):
buy: str
sell: str
emergencysell: Optional[str]
forcesell: Optional[str]
forcebuy: Optional[str]
stoploss: str
stoploss_on_exchange: bool
stoploss_on_exchange_interval: Optional[int]
class ShowConfig(BaseModel):
version: str
dry_run: bool
stake_currency: str
stake_amount: Union[float, str]
@ -136,6 +156,8 @@ class ShowConfig(BaseModel):
trailing_stop_positive: Optional[float]
trailing_stop_positive_offset: Optional[float]
trailing_only_offset_is_reached: Optional[bool]
unfilledtimeout: UnfilledTimeout
order_types: OrderTypes
use_custom_stoploss: Optional[bool]
timeframe: Optional[str]
timeframe_ms: int

View File

@ -9,9 +9,11 @@ from typing import Any, Dict, List, Optional, Tuple, Union
import arrow
import psutil
from dateutil.relativedelta import relativedelta
from numpy import NAN, inf, int64, mean
from pandas import DataFrame
from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
from freqtrade.data.history import load_data
@ -104,6 +106,7 @@ class RPC:
information via rpc.
"""
val = {
'version': __version__,
'dry_run': config['dry_run'],
'stake_currency': config['stake_currency'],
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
@ -117,7 +120,9 @@ class RPC:
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
'unfilledtimeout': config.get('unfilledtimeout'),
'use_custom_stoploss': config.get('use_custom_stoploss'),
'order_types': config.get('order_types'),
'bot_name': config.get('bot_name', 'freqtrade'),
'timeframe': config.get('timeframe'),
'timeframe_ms': timeframe_to_msecs(config['timeframe']
@ -222,9 +227,8 @@ class RPC:
trade.pair, refresh=False, side=closing_side)
except (PricingError, ExchangeError):
current_rate = NAN
trade_percent = (100 * trade.calc_profit_ratio(current_rate))
trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade_percent:.2f}%'
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
if self._fiat_converter:
fiat_profit = self._fiat_converter.convert_amount(
trade_profit,
@ -253,7 +257,7 @@ class RPC:
def _rpc_daily_profit(
self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
today = datetime.utcnow().date()
today = datetime.now(timezone.utc).date()
profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0):
@ -292,6 +296,91 @@ class RPC:
'data': data
}
def _rpc_weekly_profit(
self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
today = datetime.now(timezone.utc).date()
first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday
profit_weeks: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0):
raise RPCException('timescale must be an integer greater than 0')
for week in range(0, timescale):
profitweek = first_iso_day_of_week - timedelta(weeks=week)
trades = Trade.get_trades(trade_filter=[
Trade.is_open.is_(False),
Trade.close_date >= profitweek,
Trade.close_date < (profitweek + timedelta(weeks=1))
]).order_by(Trade.close_date).all()
curweekprofit = sum(
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
profit_weeks[profitweek] = {
'amount': curweekprofit,
'trades': len(trades)
}
data = [
{
'date': key,
'abs_profit': value["amount"],
'fiat_value': self._fiat_converter.convert_amount(
value['amount'],
stake_currency,
fiat_display_currency
) if self._fiat_converter else 0,
'trade_count': value["trades"],
}
for key, value in profit_weeks.items()
]
return {
'stake_currency': stake_currency,
'fiat_display_currency': fiat_display_currency,
'data': data
}
def _rpc_monthly_profit(
self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
first_day_of_month = datetime.now(timezone.utc).date().replace(day=1)
profit_months: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0):
raise RPCException('timescale must be an integer greater than 0')
for month in range(0, timescale):
profitmonth = first_day_of_month - relativedelta(months=month)
trades = Trade.get_trades(trade_filter=[
Trade.is_open.is_(False),
Trade.close_date >= profitmonth,
Trade.close_date < (profitmonth + relativedelta(months=1))
]).order_by(Trade.close_date).all()
curmonthprofit = sum(
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
profit_months[profitmonth] = {
'amount': curmonthprofit,
'trades': len(trades)
}
data = [
{
'date': f"{key.year}-{key.month:02d}",
'abs_profit': value["amount"],
'fiat_value': self._fiat_converter.convert_amount(
value['amount'],
stake_currency,
fiat_display_currency
) if self._fiat_converter else 0,
'trade_count': value["trades"],
}
for key, value in profit_months.items()
]
return {
'stake_currency': stake_currency,
'fiat_display_currency': fiat_display_currency,
'data': data
}
def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
""" Returns the X last trades """
order_by = Trade.id if order_by_id else Trade.close_date.desc()
@ -448,7 +537,8 @@ class RPC:
'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0,
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
'best_pair': best_pair[0] if best_pair else '',
'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0,
'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, # Deprecated
'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
'winning_trades': winning_trades,
'losing_trades': losing_trades,
}
@ -824,15 +914,15 @@ class RPC:
if has_content:
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
# Move open to separate column when signal for easy plotting
# Move signal close to separate column when signal for easy plotting
if 'buy' in dataframe.columns:
buy_mask = (dataframe['buy'] == 1)
buy_signals = int(buy_mask.sum())
dataframe.loc[buy_mask, '_buy_signal_open'] = dataframe.loc[buy_mask, 'open']
dataframe.loc[buy_mask, '_buy_signal_close'] = dataframe.loc[buy_mask, 'close']
if 'sell' in dataframe.columns:
sell_mask = (dataframe['sell'] == 1)
sell_signals = int(sell_mask.sum())
dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open']
dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close']
dataframe = dataframe.replace([inf, -inf], NAN)
dataframe = dataframe.replace({NAN: None})

View File

@ -159,6 +159,8 @@ class Telegram(RPCHandler):
CommandHandler('mix_tags', self._mix_tag_performance),
CommandHandler('stats', self._stats),
CommandHandler('daily', self._daily),
CommandHandler('weekly', self._weekly),
CommandHandler('monthly', self._monthly),
CommandHandler('count', self._count),
CommandHandler('locks', self._locks),
CommandHandler(['unlock', 'delete_locks'], self._delete_locks),
@ -175,6 +177,8 @@ class Telegram(RPCHandler):
callbacks = [
CallbackQueryHandler(self._status_table, pattern='update_status_table'),
CallbackQueryHandler(self._daily, pattern='update_daily'),
CallbackQueryHandler(self._weekly, pattern='update_weekly'),
CallbackQueryHandler(self._monthly, pattern='update_monthly'),
CallbackQueryHandler(self._profit, pattern='update_profit'),
CallbackQueryHandler(self._balance, pattern='update_balance'),
CallbackQueryHandler(self._performance, pattern='update_performance'),
@ -215,26 +219,28 @@ class Telegram(RPCHandler):
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else:
msg['stake_amount_fiat'] = 0
is_fill = msg['type'] == RPCMessageType.BUY_FILL
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
content = []
content.append(
f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
message = (
f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}"
f" (#{msg['trade_id']})\n"
)
if msg.get('buy_tag', None):
content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n")
content.append(f"*Amount:* `{msg['amount']:.8f}`\n")
content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n")
content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n")
content.append(
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
)
if msg.get('fiat_currency', None):
content.append(
f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
)
message += f"*Buy Tag:* `{msg['buy_tag']}`\n" if msg.get('buy_tag', None) else ""
message += f"*Amount:* `{msg['amount']:.8f}`\n"
if msg['type'] == RPCMessageType.BUY_FILL:
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
elif msg['type'] == RPCMessageType.BUY:
message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
if msg.get('fiat_currency', None):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message = ''.join(content)
message += ")`"
return message
@ -254,54 +260,57 @@ class Telegram(RPCHandler):
and self._rpc._fiat_converter):
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
msg['profit_extra'] = (' ({gain}: {profit_amount:.8f} {stake_currency}'
' / {profit_fiat:.3f} {fiat_currency})').format(**msg)
msg['profit_extra'] = (
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
else:
msg['profit_extra'] = ''
is_fill = msg['type'] == RPCMessageType.SELL_FILL
message = (
f"{msg['emoji']} *{msg['exchange']}:* "
f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n"
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
f"*Buy Tag:* `{msg['buy_tag']}`\n"
f"*Sell Reason:* `{msg['sell_reason']}`\n"
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
f"*Amount:* `{msg['amount']:.8f}`\n")
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
"*Profit:* `{profit_percent:.2f}%{profit_extra}`\n"
"*Buy Tag:* `{buy_tag}`\n"
"*Sell Reason:* `{sell_reason}`\n"
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
"*Amount:* `{amount:.8f}`\n"
"*Open Rate:* `{open_rate:.8f}`\n"
"*Current Rate:* `{current_rate:.8f}`\n"
"*Close Rate:* `{limit:.8f}`").format(**msg)
if msg['type'] == RPCMessageType.SELL:
message += (f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Close Rate:* `{msg['limit']:.8f}`")
elif msg['type'] == RPCMessageType.SELL_FILL:
message += f"*Close Rate:* `{msg['close_rate']:.8f}`"
return message
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
if msg_type == RPCMessageType.BUY:
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL]:
message = self._format_buy_msg(msg)
elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]:
message = self._format_sell_msg(msg)
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg))
elif msg_type == RPCMessageType.BUY_FILL:
message = ("\N{LARGE CIRCLE} *{exchange}:* "
"Buy order for {pair} (#{trade_id}) filled "
"for {open_rate}.".format(**msg))
elif msg_type == RPCMessageType.SELL_FILL:
message = ("\N{LARGE CIRCLE} *{exchange}:* "
"Sell order for {pair} (#{trade_id}) filled "
"for {close_rate}.".format(**msg))
elif msg_type == RPCMessageType.SELL:
message = self._format_sell_msg(msg)
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
message = (
"*Protection* triggered due to {reason}. "
"`{pair}` will be locked until `{lock_end_time}`."
).format(**msg)
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
message = (
"*Protection* triggered due to {reason}. "
"*All pairs* will be locked until `{lock_end_time}`."
).format(**msg)
elif msg_type == RPCMessageType.STATUS:
message = '*Status:* `{status}`'.format(**msg)
@ -353,7 +362,7 @@ class Telegram(RPCHandler):
elif float(msg['profit_percent']) >= 0.0:
return "\N{EIGHT SPOKED ASTERISK}"
elif msg['sell_reason'] == "stop_loss":
return"\N{WARNING SIGN}"
return "\N{WARNING SIGN}"
else:
return "\N{CROSS MARK}"
@ -393,19 +402,19 @@ class Telegram(RPCHandler):
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
"*Current Rate:* `{current_rate:.8f}`",
("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
+ "`{profit_pct:.2f}%`",
+ "`{profit_ratio:.2%}`",
]
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
and r['initial_stop_loss_pct'] is not None):
and r['initial_stop_loss_ratio'] is not None):
# Adding initial stoploss only if it is different from stoploss
lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
"`({initial_stop_loss_pct:.2f}%)`")
"`({initial_stop_loss_ratio:.2%})`")
# Adding stoploss and stoploss percentage only if it is not None
lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""))
("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else ""))
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
"`({stoploss_current_dist_pct:.2f}%)`")
"`({stoploss_current_dist_ratio:.2%})`")
if r['open_order']:
if r['sell_order_status']:
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
@ -501,6 +510,86 @@ class Telegram(RPCHandler):
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _weekly(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /weekly <n>
Returns a weekly profit (in BTC) over the last n weeks.
:param bot: telegram bot
:param update: message update
:return: None
"""
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config.get('fiat_display_currency', '')
try:
timescale = int(context.args[0]) if context.args else 8
except (TypeError, ValueError, IndexError):
timescale = 8
try:
stats = self._rpc._rpc_weekly_profit(
timescale,
stake_cur,
fiat_disp_cur
)
stats_tab = tabulate(
[[week['date'],
f"{round_coin_value(week['abs_profit'], stats['stake_currency'])}",
f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}",
f"{week['trade_count']} trades"] for week in stats['data']],
headers=[
'Monday',
f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}',
'Trades',
],
tablefmt='simple')
message = f'<b>Weekly Profit over the last {timescale} weeks ' \
f'(starting from Monday)</b>:\n<pre>{stats_tab}</pre> '
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path="update_weekly", query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _monthly(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /monthly <n>
Returns a monthly profit (in BTC) over the last n months.
:param bot: telegram bot
:param update: message update
:return: None
"""
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config.get('fiat_display_currency', '')
try:
timescale = int(context.args[0]) if context.args else 6
except (TypeError, ValueError, IndexError):
timescale = 6
try:
stats = self._rpc._rpc_monthly_profit(
timescale,
stake_cur,
fiat_disp_cur
)
stats_tab = tabulate(
[[month['date'],
f"{round_coin_value(month['abs_profit'], stats['stake_currency'])}",
f"{month['fiat_value']:.3f} {stats['fiat_display_currency']}",
f"{month['trade_count']} trades"] for month in stats['data']],
headers=[
'Month',
f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}',
'Trades',
],
tablefmt='simple')
message = f'<b>Monthly Profit over the last {timescale} months' \
f'</b>:\n<pre>{stats_tab}</pre> '
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path="update_monthly", query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _profit(self, update: Update, context: CallbackContext) -> None:
"""
@ -528,11 +617,11 @@ class Telegram(RPCHandler):
fiat_disp_cur,
start_date)
profit_closed_coin = stats['profit_closed_coin']
profit_closed_percent_mean = stats['profit_closed_percent_mean']
profit_closed_ratio_mean = stats['profit_closed_ratio_mean']
profit_closed_percent = stats['profit_closed_percent']
profit_closed_fiat = stats['profit_closed_fiat']
profit_all_coin = stats['profit_all_coin']
profit_all_percent_mean = stats['profit_all_percent_mean']
profit_all_ratio_mean = stats['profit_all_ratio_mean']
profit_all_percent = stats['profit_all_percent']
profit_all_fiat = stats['profit_all_fiat']
trade_count = stats['trade_count']
@ -540,7 +629,7 @@ class Telegram(RPCHandler):
latest_trade_date = stats['latest_trade_date']
avg_duration = stats['avg_duration']
best_pair = stats['best_pair']
best_rate = stats['best_rate']
best_pair_profit_ratio = stats['best_pair_profit_ratio']
if stats['trade_count'] == 0:
markdown_msg = 'No trades yet.'
else:
@ -548,7 +637,7 @@ class Telegram(RPCHandler):
if stats['closed_trade_count'] > 0:
markdown_msg = ("*ROI:* Closed trades\n"
f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} "
f"({profit_closed_percent_mean:.2f}%) "
f"({profit_closed_ratio_mean:.2%}) "
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n")
else:
@ -557,7 +646,7 @@ class Telegram(RPCHandler):
markdown_msg += (
f"*ROI:* All trades\n"
f"∙ `{round_coin_value(profit_all_coin, stake_cur)} "
f"({profit_all_percent_mean:.2f}%) "
f"({profit_all_ratio_mean:.2%}) "
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
f"*Total Trade Count:* `{trade_count}`\n"
@ -568,7 +657,7 @@ class Telegram(RPCHandler):
)
if stats['closed_trade_count'] > 0:
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`")
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
query=update.callback_query)
@ -597,10 +686,16 @@ class Telegram(RPCHandler):
count['losses']
] for reason, count in stats['sell_reasons'].items()
]
sell_reasons_msg = tabulate(
sell_reasons_tabulate,
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
)
sell_reasons_msg = 'No trades yet.'
for reason in chunks(sell_reasons_tabulate, 25):
sell_reasons_msg = tabulate(
reason,
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
)
if len(sell_reasons_tabulate) > 25:
self._send_msg(sell_reasons_msg, ParseMode.MARKDOWN)
sell_reasons_msg = ''
durations = stats['durations']
duration_msg = tabulate(
[
@ -671,10 +766,10 @@ class Telegram(RPCHandler):
output += ("\n*Estimated Value*:\n"
f"\t`{result['stake']}: "
f"{round_coin_value(result['total'], result['stake'], False)}`"
f" `({result['starting_capital_pct']}%)`\n"
f" `({result['starting_capital_ratio']:.2%})`\n"
f"\t`{result['symbol']}: "
f"{round_coin_value(result['value'], result['symbol'], False)}`"
f" `({result['starting_capital_fiat_pct']}%)`\n")
f" `({result['starting_capital_fiat_ratio']:.2%})`\n")
self._send_msg(output, reload_able=True, callback_path="update_balance",
query=update.callback_query)
except RPCException as e:
@ -809,7 +904,7 @@ class Telegram(RPCHandler):
trades_tab = tabulate(
[[arrow.get(trade['close_date']).humanize(),
trade['pair'] + " (#" + str(trade['trade_id']) + ")",
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
f"{(trade['close_profit']):.2%} ({trade['close_profit_abs']})"]
for trade in trades['trades']],
headers=[
'Close Date',
@ -861,7 +956,7 @@ class Telegram(RPCHandler):
stat_line = (
f"{i+1}.\t <code>{trade['pair']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit_pct']:.2f}%) "
f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
@ -896,7 +991,7 @@ class Telegram(RPCHandler):
stat_line = (
f"{i+1}.\t <code>{trade['buy_tag']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit_pct']:.2f}%) "
f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
@ -931,7 +1026,7 @@ class Telegram(RPCHandler):
stat_line = (
f"{i+1}.\t <code>{trade['sell_reason']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit_pct']:.2f}%) "
f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
@ -966,7 +1061,7 @@ class Telegram(RPCHandler):
stat_line = (
f"{i+1}.\t <code>{trade['mix_tag']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit']:.2f}%) "
f"({trade['profit']:.2%}) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
@ -1149,44 +1244,56 @@ class Telegram(RPCHandler):
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
"Optionally takes a rate at which to buy "
"(only applies to limit orders).` \n")
message = ("*/start:* `Starts the trader`\n"
"*/stop:* `Stops the trader`\n"
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
" *<trade_id> :* `Lists one or more specific trades.`\n"
" `Separate multiple <trade_id> with a blank space.`\n"
" *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n"
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
"over the last n days`\n"
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
"regardless of profit`\n"
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
"*/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"
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
"*/balance:* `Show account balance per currency`\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/reload_config:* `Reload configuration file` \n"
"*/show_config:* `Show running configuration` \n"
"*/logs [limit]:* `Show latest logs - defaults to 10` \n"
"*/whitelist:* `Show current whitelist` \n"
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
"to the blacklist.` \n"
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
"*/help:* `This help message`\n"
"*/version:* `Show version`")
message = (
"_BotControl_\n"
"------------\n"
"*/start:* `Starts the trader`\n"
"*/stop:* Stops the trader\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
"regardless of profit`\n"
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/whitelist:* `Show current whitelist` \n"
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
"to the blacklist.` \n"
"*/reload_config:* `Reload configuration file` \n"
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
self._send_msg(message)
"_Current state_\n"
"------------\n"
"*/show_config:* `Show running configuration` \n"
"*/locks:* `Show currently locked pairs`\n"
"*/balance:* `Show account balance per currency`\n"
"*/logs [limit]:* `Show latest logs - defaults to 10` \n"
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
"_Statistics_\n"
"------------\n"
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
" *<trade_id> :* `Lists one or more specific trades.`\n"
" `Separate multiple <trade_id> with a blank space.`\n"
" *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n"
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
"over the last n days`\n"
"*/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"
"*/weekly <n>:* `Shows statistics per week, over the last n weeks`\n"
"*/monthly <n>:* `Shows statistics per month, over the last n months`\n"
"*/stats:* `Shows Wins / losses by Sell reason as well as "
"Avg. holding durationsfor buys and sells.`\n"
"*/help:* `This help message`\n"
"*/version:* `Show version`"
)
self._send_msg(message, parse_mode=ParseMode.MARKDOWN)
@authorized_only
def _version(self, update: Update, context: CallbackContext) -> None:

View File

@ -292,7 +292,7 @@ class BooleanParameter(CategoricalParameter):
load=load, **kwargs)
class HyperStrategyMixin(object):
class HyperStrategyMixin:
"""
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
strategy logic.

View File

@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
CUSTOM_SELL_MAX_LENGTH = 64
class SellCheckTuple(object):
class SellCheckTuple:
"""
NamedTuple for Sell type + reason
"""
@ -868,7 +868,7 @@ class IStrategy(ABC, HyperStrategyMixin):
if self.trailing_stop_positive is not None and bound_profit > sl_offset:
stop_loss_value = self.trailing_stop_positive
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
f"offset: {sl_offset:.4g} profit: {current_profit:.2%}")
trade.adjust_stop_loss(bound or current_rate, stop_loss_value)

View File

@ -1,4 +1,5 @@
import logging
from copy import deepcopy
from freqtrade.exceptions import StrategyError
@ -14,6 +15,9 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_err
"""
def wrapper(*args, **kwargs):
try:
if 'trade' in kwargs:
# Protect accidental modifications from within the strategy
kwargs['trade'] = deepcopy(kwargs['trade'])
return f(*args, **kwargs)
except ValueError as error:
logger.warning(

View File

@ -73,7 +73,7 @@ class Wallets:
tot_profit = Trade.get_total_closed_profit()
else:
tot_profit = LocalTrade.total_profit
tot_in_trades = sum([trade.stake_amount for trade in open_trades])
tot_in_trades = sum(trade.stake_amount for trade in open_trades)
current_stake = self.start_cap + tot_profit - tot_in_trades
_wallets[self._config['stake_currency']] = Wallet(
@ -238,7 +238,7 @@ class Wallets:
return self._check_available_stake_amount(stake_amount, available_amount)
def _validate_stake_amount(self, pair, stake_amount, min_stake_amount):
def validate_stake_amount(self, pair, stake_amount, min_stake_amount):
if not stake_amount:
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
return 0
@ -250,17 +250,27 @@ class Wallets:
logger.warning("Minimum stake amount > available balance.")
return 0
if min_stake_amount is not None and stake_amount < min_stake_amount:
stake_amount = min_stake_amount
if self._log:
logger.info(
f"Stake amount for pair {pair} is too small "
f"({stake_amount} < {min_stake_amount}), adjusting to {min_stake_amount}."
)
if stake_amount * 1.3 < min_stake_amount:
# Top-cap stake-amount adjustments to +30%.
if self._log:
logger.info(
f"Adjusted stake amount for pair {pair} is more than 30% bigger than "
f"the desired stake ({stake_amount} * 1.3 > {max_stake_amount}), "
f"ignoring trade."
)
return 0
stake_amount = min_stake_amount
if stake_amount > max_stake_amount:
stake_amount = max_stake_amount
if self._log:
logger.info(
f"Stake amount for pair {pair} is too big "
f"({stake_amount} > {max_stake_amount}), adjusting to {max_stake_amount}."
)
stake_amount = max_stake_amount
return stake_amount

View File

@ -3,7 +3,7 @@
-r requirements-plot.txt
-r requirements-hyperopt.txt
coveralls==3.2.0
coveralls==3.3.1
flake8==4.0.1
flake8-tidy-imports==4.5.0
mypy==0.910
@ -12,15 +12,18 @@ pytest-asyncio==0.16.0
pytest-cov==3.0.0
pytest-mock==3.6.1
pytest-random-order==1.0.4
isort==5.9.3
isort==5.10.1
# For datetime mocking
time-machine==2.4.0
# Convert jupyter notebooks to markdown documents
nbconvert==6.2.0
nbconvert==6.3.0
# mypy types
types-cachetools==4.2.4
types-filelock==3.2.1
types-requests==2.25.11
types-requests==2.26.0
types-tabulate==0.8.3
# Extensions to datetime library
types-python-dateutil==2.8.2

View File

@ -2,7 +2,7 @@
-r requirements.txt
# Required for hyperopt
scipy==1.7.1
scipy==1.7.2
scikit-learn==1.0.1
scikit-optimize==0.9.0
filelock==3.3.2

View File

@ -1,4 +1,4 @@
numpy==1.21.3
numpy==1.21.4
pandas==1.3.4
pandas-ta==0.3.14b
@ -6,18 +6,18 @@ ccxt==1.61.24
# Pin cryptography for now due to rust build errors with piwheels
cryptography==35.0.0
aiohttp==3.7.4.post0
SQLAlchemy==1.4.26
python-telegram-bot==13.7
SQLAlchemy==1.4.27
python-telegram-bot==13.8.1
arrow==1.2.1
cachetools==4.2.2
requests==2.26.0
urllib3==1.26.7
jsonschema==4.1.2
jsonschema==4.2.1
TA-Lib==0.4.21
technical==1.3.0
tabulate==0.8.9
pycoingecko==2.2.0
jinja2==3.0.2
jinja2==3.0.3
tables==3.6.1
blosc==1.10.6
@ -41,7 +41,9 @@ psutil==5.8.0
colorama==0.4.4
# Building config files interactively
questionary==1.10.0
prompt-toolkit==3.0.21
prompt-toolkit==3.0.22
# Extensions to datetime library
python-dateutil==2.8.2
#Futures
schedule==1.1.0

View File

@ -39,7 +39,7 @@ class FtRestClient():
def _call(self, method, apipath, params: dict = None, data=None, files=None):
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
raise ValueError('invalid method <{0}>'.format(method))
raise ValueError(f'invalid method <{method}>')
basepath = f"{self._serverurl}/api/v1/{apipath}"
hd = {"Accept": "application/json",
@ -124,7 +124,7 @@ class FtRestClient():
:param lock_id: ID for the lock to delete
:return: json object
"""
return self._delete("locks/{}".format(lock_id))
return self._delete(f"locks/{lock_id}")
def daily(self, days=None):
"""Return the profits for each day, and amount of trades.
@ -220,7 +220,7 @@ class FtRestClient():
:param trade_id: Specify which trade to get.
:return: json object
"""
return self._get("trade/{}".format(trade_id))
return self._get(f"trade/{trade_id}")
def delete_trade(self, trade_id):
"""Delete trade from the database.
@ -229,7 +229,7 @@ class FtRestClient():
:param trade_id: Deletes the trade with this ID from the database.
:return: json object
"""
return self._delete("trades/{}".format(trade_id))
return self._delete(f"trades/{trade_id}")
def whitelist(self):
"""Show the current whitelist.

View File

@ -17,7 +17,7 @@ from telegram import Chat, Message, Update
from freqtrade import constants
from freqtrade.commands import Arguments
from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo
from freqtrade.edge import PairInfo
from freqtrade.enums import Collateral, RunMode, TradingMode
from freqtrade.enums.signaltype import SignalDirection
from freqtrade.exchange import Exchange
@ -163,11 +163,6 @@ def patch_edge(mocker) -> None:
mocker.patch('freqtrade.edge.Edge.calculate', MagicMock(return_value=True))
def get_patched_edge(mocker, config) -> Edge:
patch_edge(mocker)
edge = Edge(config)
return edge
# Functions for recurrent object patching
@ -2370,6 +2365,46 @@ def market_buy_order_usdt():
}
@pytest.fixture
def market_buy_order_usdt_doublefee(market_buy_order_usdt):
order = deepcopy(market_buy_order_usdt)
order['fee'] = None
# Market orders filled with 2 trades can have fees in different currencies
# assuming the account runs out of BNB.
order['fees'] = [
{'cost': 0.00025125, 'currency': 'BNB'},
{'cost': 0.05030681, 'currency': 'USDT'},
]
order['trades'] = [{
'timestamp': None,
'datetime': None,
'symbol': 'ETH/USDT',
'id': None,
'order': '123',
'type': 'market',
'side': 'sell',
'takerOrMaker': None,
'price': 2.01,
'amount': 25.0,
'cost': 50.25,
'fee': {'cost': 0.00025125, 'currency': 'BNB'}
}, {
'timestamp': None,
'datetime': None,
'symbol': 'ETH/USDT',
'id': None,
'order': '123',
'type': 'market',
'side': 'sell',
'takerOrMaker': None,
'price': 2.0,
'amount': 5,
'cost': 10,
'fee': {'cost': 0.0100306, 'currency': 'USDT'}
}]
return order
@pytest.fixture
def market_sell_order_usdt():
return {

View File

@ -360,13 +360,16 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog):
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = 'ETH/BTC'
res = await exchange._async_get_historic_ohlcv(pair, "5m",
1500000000000, is_new_pair=False)
respair, restf, res = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, is_new_pair=False)
assert respair == pair
assert restf == '5m'
# Call with very old timestamp - causes tons of requests
assert exchange._api_async.fetch_ohlcv.call_count > 400
# assert res == ohlcv
exchange._api_async.fetch_ohlcv.reset_mock()
res = await exchange._async_get_historic_ohlcv(pair, "5m", 1500000000000, is_new_pair=True)
_, _, res = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, is_new_pair=True)
# Called twice - one "init" call - and one to get the actual data.
assert exchange._api_async.fetch_ohlcv.call_count == 2
@ -375,7 +378,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog):
@pytest.mark.parametrize("trading_mode,collateral,config", [
("", "", {}),
("spot", "", {}),
("margin", "cross", {"options": {"defaultType": "margin"}}),
("futures", "isolated", {"options": {"defaultType": "future"}}),
])

View File

@ -240,9 +240,9 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
(2.9999, 4, 0.005, 2.995),
])
def test_amount_to_precision(default_conf, mocker, amount, precision_mode, precision, expected):
'''
"""
Test rounds down
'''
"""
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}})
@ -281,9 +281,7 @@ def test_amount_to_precision(default_conf, mocker, amount, precision_mode, preci
])
def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected):
'''
Test price to precision
'''
"""Test price to precision"""
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': precision}}})
exchange = get_patched_exchange(mocker, default_conf, id="binance")
@ -967,9 +965,22 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
default_conf['startup_candle_count'] = 20
ex = Exchange(default_conf)
assert ex
default_conf['startup_candle_count'] = 600
# assumption is that the exchange provides 500 candles per call.s
assert ex.validate_required_startup_candles(200, '5m') == 1
assert ex.validate_required_startup_candles(499, '5m') == 1
assert ex.validate_required_startup_candles(600, '5m') == 2
assert ex.validate_required_startup_candles(501, '5m') == 2
assert ex.validate_required_startup_candles(499, '5m') == 1
assert ex.validate_required_startup_candles(1000, '5m') == 3
assert ex.validate_required_startup_candles(2499, '5m') == 5
assert log_has_re(r'Using 5 calls to get OHLCV. This.*', caplog)
with pytest.raises(OperationalException, match=r'This strategy requires 600.*'):
with pytest.raises(OperationalException, match=r'This strategy requires 2500.*'):
ex.validate_required_startup_candles(2500, '5m')
# Ensure the same also happens on init
default_conf['startup_candle_count'] = 6000
with pytest.raises(OperationalException, match=r'This strategy requires 6000.*'):
Exchange(default_conf)
@ -1570,6 +1581,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above OHLCV data
assert len(ret) == 2
assert log_has_re(r'Downloaded data for .* with length .*\.', caplog)
caplog.clear()
@ -1651,12 +1663,13 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = 'ETH/USDT'
res = await exchange._async_get_historic_ohlcv(pair, "5m",
1500000000000, is_new_pair=False)
respair, restf, res = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, is_new_pair=False)
assert respair == pair
assert restf == '5m'
# Call with very old timestamp - causes tons of requests
assert exchange._api_async.fetch_ohlcv.call_count > 200
assert res[0] == ohlcv[0]
assert log_has_re(r'Downloaded data for .* with length .*\.', caplog)
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
@ -1694,12 +1707,14 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
assert exchange._api_async.fetch_ohlcv.call_count == 2
exchange._api_async.fetch_ohlcv.reset_mock()
exchange.required_candle_call_count = 2
res = exchange.refresh_latest_ohlcv(pairs)
assert len(res) == len(pairs)
assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog)
assert exchange._klines
assert exchange._api_async.fetch_ohlcv.call_count == 2
assert exchange._api_async.fetch_ohlcv.call_count == 4
exchange._api_async.fetch_ohlcv.reset_mock()
for pair in pairs:
assert isinstance(exchange.klines(pair), DataFrame)
assert len(exchange.klines(pair)) > 0
@ -1715,7 +1730,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')])
assert len(res) == len(pairs)
assert exchange._api_async.fetch_ohlcv.call_count == 2
assert exchange._api_async.fetch_ohlcv.call_count == 0
assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
f"timeframe {pairs[0][1]} ...",
caplog)
@ -2066,15 +2081,6 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog):
assert exchange.get_rate(pair, refresh=True, side="sell") == 0.13
def make_fetch_ohlcv_mock(data):
def fetch_ohlcv_mock(pair, timeframe, since):
if since:
assert since > data[-1][0]
return []
return data
return fetch_ohlcv_mock
@pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.asyncio
async def test___async_get_candle_history_sort(default_conf, mocker, exchange_name):

View File

@ -427,6 +427,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13)))
patch_exchange(mocker)
mocker.patch.object(Path, 'open')
mocker.patch('freqtrade.configuration.config_validation.validate_config_schema')
mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None})
optimizer_param = {

View File

@ -1018,7 +1018,7 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
assert len(res) == 1
assert res[0]['mix_tag'] == 'Other Other'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 6.2)
assert prec_satoshi(res[0]['profit_pct'], 6.2)
trade.buy_tag = "TESTBUY"
trade.sell_reason = "TESTSELL"
@ -1027,7 +1027,7 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
assert len(res) == 1
assert res[0]['mix_tag'] == 'TESTBUY TESTSELL'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 6.2)
assert prec_satoshi(res[0]['profit_pct'], 6.2)
def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
@ -1046,10 +1046,10 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
assert len(res) == 2
assert res[0]['mix_tag'] == 'TEST1 sell_signal'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 0.5)
assert prec_satoshi(res[0]['profit_pct'], 0.5)
assert res[1]['mix_tag'] == 'Other roi'
assert res[1]['count'] == 1
assert prec_satoshi(res[1]['profit'], 1.0)
assert prec_satoshi(res[1]['profit_pct'], 1.0)
# Test for a specific pair
res = rpc._rpc_mix_tag_performance('ETC/BTC')
@ -1057,7 +1057,7 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
assert len(res) == 1
assert res[0]['count'] == 1
assert res[0]['mix_tag'] == 'TEST1 sell_signal'
assert prec_satoshi(res[0]['profit'], 0.5)
assert prec_satoshi(res[0]['profit_pct'], 0.5)
def test_rpc_count(mocker, default_conf, ticker, fee) -> None:

View File

@ -521,7 +521,7 @@ def test_api_locks(botclient):
assert rc.json()['lock_count'] == 0
def test_api_show_config(botclient, mocker):
def test_api_show_config(botclient):
ftbot, client = botclient
patch_get_signal(ftbot)
@ -537,6 +537,8 @@ def test_api_show_config(botclient, mocker):
assert not rc.json()['trailing_stop']
assert 'bid_strategy' in rc.json()
assert 'ask_strategy' in rc.json()
assert 'unfilledtimeout' in rc.json()
assert 'version' in rc.json()
def test_api_daily(botclient, mocker, ticker, fee, markets):
@ -704,7 +706,8 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
'is_short,expected',
[(
True,
{'best_pair': 'ETC/BTC', 'best_rate': -0.5, 'profit_all_coin': 43.61269123,
{'best_pair': 'ETC/BTC', 'best_rate': -0.5, 'best_pair_profit_ratio': -0.005,
'profit_all_coin': 43.61269123,
'profit_all_fiat': 538398.67323435, 'profit_all_percent_mean': 66.41,
'profit_all_ratio_mean': 0.664109545, 'profit_all_percent_sum': 398.47,
'profit_all_ratio_sum': 3.98465727, 'profit_all_percent': 4.36,
@ -716,7 +719,8 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
),
(
False,
{'best_pair': 'XRP/BTC', 'best_rate': 1.0, 'profit_all_coin': -44.0631579,
{'best_pair': 'XRP/BTC', 'best_rate': 1.0, 'best_pair_profit_ratio': 0.01,
'profit_all_coin': -44.0631579,
'profit_all_fiat': -543959.6842755, 'profit_all_percent_mean': -66.41,
'profit_all_ratio_mean': -0.6641100666666667, 'profit_all_percent_sum': -398.47,
'profit_all_ratio_sum': -3.9846604, 'profit_all_percent': -4.41,
@ -728,7 +732,8 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
),
(
None,
{'best_pair': 'XRP/BTC', 'best_rate': 1.0, 'profit_all_coin': -14.43790415,
{'best_pair': 'XRP/BTC', 'best_rate': 1.0, 'best_pair_profit_ratio': 0.01,
'profit_all_coin': -14.43790415,
'profit_all_fiat': -178235.92673175, 'profit_all_percent_mean': 0.08,
'profit_all_ratio_mean': 0.000835751666666662, 'profit_all_percent_sum': 0.5,
'profit_all_ratio_sum': 0.005014509999999972, 'profit_all_percent': -1.44,
@ -763,6 +768,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
assert rc.json() == {
'avg_duration': ANY,
'best_pair': expected['best_pair'],
'best_pair_profit_ratio': expected['best_pair_profit_ratio'],
'best_rate': expected['best_rate'],
'first_trade_date': ANY,
'first_trade_timestamp': ANY,
@ -1185,7 +1191,7 @@ def test_api_pair_candles(botclient, ohlcv_history):
assert isinstance(rc.json()['columns'], list)
assert rc.json()['columns'] == ['date', 'open', 'high',
'low', 'close', 'volume', 'sma', 'buy', 'sell',
'__date_ts', '_buy_signal_open', '_sell_signal_open']
'__date_ts', '_buy_signal_close', '_sell_signal_close']
assert 'pair' in rc.json()
assert rc.json()['pair'] == 'XRP/BTC'
@ -1196,7 +1202,8 @@ def test_api_pair_candles(botclient, ohlcv_history):
[['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869,
None, 0, 0, 1511686200000, None, None],
['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05,
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None],
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.893e-05,
None],
['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
0.7039405, 8.885e-05, 0, 0, 1511686800000, None, None]

View File

@ -4,7 +4,7 @@
import logging
import re
from datetime import datetime
from datetime import datetime, timedelta
from functools import reduce
from random import choice, randint
from string import ascii_uppercase
@ -94,10 +94,11 @@ 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'], ['buys'], ['sells'], ['mix_tags'], "
"['stats'], ['daily'], ['count'], ['locks'], "
"['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], "
"['show_config', 'show_conf'], ['stopbuy'], "
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']"
"['stats'], ['daily'], ['weekly'], ['monthly'], "
"['count'], ['locks'], ['unlock', 'delete_locks'], "
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
"['stopbuy'], ['whitelist'], ['blacklist'], "
"['logs'], ['edge'], ['help'], ['version']"
"]")
assert log_has(message_str, caplog)
@ -188,16 +189,16 @@ def test_telegram_status(default_conf, update, mocker) -> None:
'amount': 90.99181074,
'stake_amount': 90.99181074,
'buy_tag': None,
'close_profit_pct': None,
'close_profit_ratio': None,
'profit': -0.0059,
'profit_pct': -0.59,
'profit_ratio': -0.0059,
'initial_stop_loss_abs': 1.098e-05,
'stop_loss_abs': 1.099e-05,
'sell_order_status': None,
'initial_stop_loss_pct': -0.05,
'initial_stop_loss_ratio': -0.0005,
'stoploss_current_dist': 1e-08,
'stoploss_current_dist_pct': -0.02,
'stop_loss_pct': -0.01,
'stoploss_current_dist_ratio': -0.0002,
'stop_loss_ratio': -0.0001,
'open_order': '(limit buy rem=0.00000000)',
'is_open': True
}]),
@ -354,7 +355,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
context.args = ["2"]
telegram._daily(update=update, context=context)
assert msg_mock.call_count == 1
assert 'Daily' in msg_mock.call_args_list[0][0][0]
assert "Daily Profit over the last 2 days</b>:" in msg_mock.call_args_list[0][0][0]
assert 'Day ' in msg_mock.call_args_list[0][0][0]
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
@ -366,7 +368,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
context.args = []
telegram._daily(update=update, context=context)
assert msg_mock.call_count == 1
assert 'Daily' in msg_mock.call_args_list[0][0][0]
assert "Daily Profit over the last 7 days</b>:" in msg_mock.call_args_list[0][0][0]
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
@ -422,7 +424,242 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
context = MagicMock()
context.args = ["today"]
telegram._daily(update=update, context=context)
assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0]
assert str('Daily Profit over the last 7 days</b>:') in msg_mock.call_args_list[0][0][0]
def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
default_conf['max_open_trades'] = 1
mocker.patch(
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
return_value=15000.0
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
# Try valid data
# /weekly 2
context = MagicMock()
context.args = ["2"]
telegram._weekly(update=update, context=context)
assert msg_mock.call_count == 1
assert "Weekly Profit over the last 2 weeks (starting from Monday)</b>:" \
in msg_mock.call_args_list[0][0][0]
assert 'Monday ' in msg_mock.call_args_list[0][0][0]
today = datetime.utcnow().date()
first_iso_day_of_current_week = today - timedelta(days=today.weekday())
assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
# Reset msg_mock
msg_mock.reset_mock()
context.args = []
telegram._weekly(update=update, context=context)
assert msg_mock.call_count == 1
assert "Weekly Profit over the last 8 weeks (starting from Monday)</b>:" \
in msg_mock.call_args_list[0][0][0]
assert 'Weekly' in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
# Reset msg_mock
msg_mock.reset_mock()
freqtradebot.config['max_open_trades'] = 2
# Add two other trades
n = freqtradebot.enter_positions()
assert n == 2
trades = Trade.query.all()
for trade in trades:
trade.update(limit_buy_order)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
# /weekly 1
# By default, the 8 previous weeks are shown
# So the previous modified trade should be excluded from the stats
context = MagicMock()
context.args = ["1"]
telegram._weekly(update=update, context=context)
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Try invalid data
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
# /weekly -3
context = MagicMock()
context.args = ["-3"]
telegram._weekly(update=update, context=context)
assert msg_mock.call_count == 1
assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0]
# Try invalid data
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
# /weekly this week
context = MagicMock()
context.args = ["this week"]
telegram._weekly(update=update, context=context)
assert str('Weekly Profit over the last 8 weeks (starting from Monday)</b>:') \
in msg_mock.call_args_list[0][0][0]
def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
default_conf['max_open_trades'] = 1
mocker.patch(
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
return_value=15000.0
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
# Try valid data
# /monthly 2
context = MagicMock()
context.args = ["2"]
telegram._monthly(update=update, context=context)
assert msg_mock.call_count == 1
assert 'Monthly Profit over the last 2 months</b>:' in msg_mock.call_args_list[0][0][0]
assert 'Month ' in msg_mock.call_args_list[0][0][0]
today = datetime.utcnow().date()
current_month = f"{today.year}-{today.month} "
assert current_month in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
# Reset msg_mock
msg_mock.reset_mock()
context.args = []
telegram._monthly(update=update, context=context)
assert msg_mock.call_count == 1
# Default to 6 months
assert 'Monthly Profit over the last 6 months</b>:' in msg_mock.call_args_list[0][0][0]
assert 'Month ' in msg_mock.call_args_list[0][0][0]
assert current_month in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
# Reset msg_mock
msg_mock.reset_mock()
freqtradebot.config['max_open_trades'] = 2
# Add two other trades
n = freqtradebot.enter_positions()
assert n == 2
trades = Trade.query.all()
for trade in trades:
trade.update(limit_buy_order)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
# /monthly 12
context = MagicMock()
context.args = ["12"]
telegram._monthly(update=update, context=context)
assert msg_mock.call_count == 1
assert 'Monthly Profit over the last 12 months</b>:' in msg_mock.call_args_list[0][0][0]
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
# The one-digit months should contain a zero, Eg: September 2021 = "2021-09"
# Since we loaded the last 12 months, any month should appear
assert str('-09') in msg_mock.call_args_list[0][0][0]
def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Try invalid data
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
# /monthly -3
context = MagicMock()
context.args = ["-3"]
telegram._monthly(update=update, context=context)
assert msg_mock.call_count == 1
assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0]
# Try invalid data
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
# /monthly february
context = MagicMock()
context.args = ["february"]
telegram._monthly(update=update, context=context)
assert str('Monthly Profit over the last 6 months</b>:') in msg_mock.call_args_list[0][0][0]
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
@ -495,7 +732,7 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
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]
assert 'No trades yet.' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
# Create some test data
@ -1453,17 +1690,25 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
telegram.send_msg({
'type': RPCMessageType.BUY_FILL,
'buy_tag': 'buy_signal_01',
'trade_id': 1,
'buy_tag': 'buy_signal_01',
'exchange': 'Binance',
'pair': 'ETH/USDT',
'open_rate': 200,
'stake_amount': 100,
'amount': 0.5,
'open_date': arrow.utcnow().datetime
'pair': 'ETH/BTC',
'stake_amount': 0.001,
# 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
'open_rate': 1.099e-05,
'amount': 1333.3333333333335,
'open_date': arrow.utcnow().shift(hours=-1)
})
assert (msg_mock.call_args[0][0] == '\N{LARGE CIRCLE} *Binance:* '
'Buy order for ETH/USDT (#1) filled for 200.')
assert msg_mock.call_args[0][0] \
== '\N{CHECK MARK} *Binance:* Bought ETH/BTC (#1)\n' \
'*Buy Tag:* `buy_signal_01`\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \
'*Total:* `(0.00100000 BTC, 12.345 USD)`'
def test_send_msg_sell_notification(default_conf, mocker) -> None:
@ -1494,7 +1739,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
})
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
'*Buy Tag:* `buy_signal1`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `1:00:00 (60.0 min)`\n'
@ -1526,7 +1771,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
})
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Profit:* `-57.41%`\n'
'*Unrealized Profit:* `-57.41%`\n'
'*Buy Tag:* `buy_signal1`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
@ -1580,25 +1825,30 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None:
'type': RPCMessageType.SELL_FILL,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'ETH/USDT',
'pair': 'KEY/ETH',
'gain': 'loss',
'limit': 3.201e-05,
'amount': 0.1,
'amount': 1333.3333333333335,
'order_type': 'market',
'open_rate': 500,
'close_rate': 550,
'current_rate': 3.201e-05,
'open_rate': 7.5e-05,
'close_rate': 3.201e-05,
'profit_amount': -0.05746268,
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD',
'buy_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(hours=-1),
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] \
== ('\N{LARGE CIRCLE} *Binance:* Sell order for ETH/USDT (#1) filled for 550.')
== ('\N{WARNING SIGN} *Binance:* Sold KEY/ETH (#1)\n'
'*Profit:* `-57.41%`\n'
'*Buy Tag:* `buy_signal1`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
'*Amount:* `1333.33333333`\n'
'*Close Rate:* `0.00003201`'
)
def test_send_msg_status_notification(default_conf, mocker) -> None:
@ -1690,7 +1940,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Profit:* `-57.41%`\n'
'*Unrealized Profit:* `-57.41%`\n'
'*Buy Tag:* `buy_signal1`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `2:35:03 (155.1 min)`\n'

View File

@ -171,12 +171,8 @@ def test_edge_called_in_process(mocker, edge_conf) -> None:
patch_RPCManager(mocker)
patch_edge(mocker)
def _refresh_whitelist(list):
return ['ETH/USDT', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC']
patch_exchange(mocker)
freqtrade = FreqtradeBot(edge_conf)
freqtrade.pairlists._validate_whitelist = _refresh_whitelist
patch_get_signal(freqtrade)
freqtrade.process()
assert freqtrade.active_pair_whitelist == ['NEO/BTC', 'LTC/BTC']
@ -328,7 +324,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke
@pytest.mark.parametrize("is_short", [False, True])
@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
(5.0, True, True, 99),
(0.00005, True, False, 99),
(0.049, True, False, 99), # Amount will be adjusted to min - which is 0.051
(0, False, True, 99),
(UNLIMITED_STAKE_AMOUNT, False, True, 0),
])
@ -678,9 +674,6 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
patch_RPCManager(mocker)
patch_exchange(mocker)
def _refresh_whitelist(list):
return ['ETH/USDT', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC']
refresh_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@ -697,7 +690,6 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
mocker.patch('time.sleep', return_value=None)
freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.pairlists._validate_whitelist = _refresh_whitelist
freqtrade.strategy.informative_pairs = inf_pairs
# patch_get_signal(freqtrade)
@ -1733,7 +1725,6 @@ def test_exit_positions_exception(
trade = MagicMock()
trade.is_short = is_short
trade.open_order_id = None
trade.open_fee = 0.001
trade.pair = 'ETH/USDT'
trades = [trade]
@ -1853,8 +1844,6 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit
trade = MagicMock()
trade.open_order_id = '123'
trade.open_fee = 0.001
trade.is_short = is_short
# Test raise of OperationalException exception
mocker.patch(
@ -1872,7 +1861,6 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) ->
trade = MagicMock()
trade.open_order_id = '123'
trade.open_fee = 0.001
# Test raise of OperationalException exception
grm_mock = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", MagicMock())
@ -2364,12 +2352,13 @@ def test_check_handle_timedout_buy_exception(
@pytest.mark.parametrize("is_short", [False, True])
def test_check_handle_timedout_sell_usercustom(
default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker,
is_short, open_trade_usdt
is_short, open_trade_usdt, caplog
) -> None:
default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440}
default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1}
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock()
patch_exchange(mocker)
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt,
@ -2412,6 +2401,14 @@ def test_check_handle_timedout_sell_usercustom(
assert open_trade_usdt.is_open is True
assert freqtrade.strategy.check_sell_timeout.call_count == 1
# 2nd canceled trade ...
caplog.clear()
open_trade_usdt.open_order_id = 'order_id_2'
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
freqtrade.check_handle_timedout()
assert log_has_re('Emergencyselling trade.*', caplog)
assert et_mock.call_count == 1
@pytest.mark.parametrize("is_short", [False, True])
def test_check_handle_timedout_sell(
@ -2837,6 +2834,8 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
)
assert rpc_mock.call_count == 0
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
assert id(freqtrade.strategy.confirm_trade_exit.call_args_list[0][1]['trade']) != id(trade)
assert freqtrade.strategy.confirm_trade_exit.call_args_list[0][1]['trade'].id == trade.id
# Repatch with true
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
@ -3700,7 +3699,7 @@ def test_trailing_stop_loss_positive(
# stop-loss not reached, adjusted stoploss
assert freqtrade.handle_trade(trade) is False
caplog_text = (f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: "
f"{'0.0249' if not is_short else '0.0224'}%")
f"{'2.49' if not is_short else '2.24'}%")
if trail_if_reached:
assert not log_has(caplog_text, caplog)
assert not log_has("ETH/USDT - Adjusting stoploss...", caplog)
@ -3721,7 +3720,7 @@ def test_trailing_stop_loss_positive(
assert freqtrade.handle_trade(trade) is False
assert log_has(
f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: "
f"{'0.0572' if not is_short else '0.0567'}%",
f"{'5.72' if not is_short else '5.67'}%",
caplog
)
assert log_has("ETH/USDT - Adjusting stoploss...", caplog)
@ -3997,6 +3996,31 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_
assert freqtrade.get_real_amount(trade, limit_buy_order_usdt) == amount
def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_doublefee,
fee, mocker):
tfo_mock = mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', return_value='BNB/USDT')
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 200})
trade = Trade(
pair='LTC/USDT',
amount=30.0,
exchange='binance',
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
# Amount does not change
assert trade.fee_open == 0.0025
assert freqtrade.get_real_amount(trade, market_buy_order_usdt_doublefee) == 30.0
assert tfo_mock.call_count == 0
# Fetch fees from trades dict if available to get "proper" values
assert round(trade.fee_open, 4) == 0.001
def test_get_real_amount_wrong_amount(default_conf_usdt, trades_for_order, buy_order_fee, fee,
mocker):
limit_buy_order_usdt = deepcopy(buy_order_fee)

View File

@ -14,8 +14,8 @@ from freqtrade import constants
from freqtrade.enums import TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db
from tests.conftest import (create_mock_trades, create_mock_trades_with_leverage, get_sides,
log_has, log_has_re)
from tests.conftest import (create_mock_trades, create_mock_trades_usdt,
create_mock_trades_with_leverage, get_sides, log_has, log_has_re)
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
@ -1980,6 +1980,13 @@ def test_get_best_pair_lev(fee):
assert res[1] == 0.1713156134055116
def test_get_exit_order_count(fee):
create_mock_trades_usdt(fee)
trade = Trade.get_trades([Trade.pair == 'ETC/USDT']).first()
assert trade.get_exit_order_count() == 1
@pytest.mark.usefixtures("init_persistence")
def test_update_order_from_ccxt(caplog):
# Most basic order return (only has orderid)

View File

@ -1,4 +1,3 @@
from copy import deepcopy
from pathlib import Path
from unittest.mock import MagicMock
@ -172,7 +171,7 @@ def test_plot_trades(testdatadir, caplog):
assert len(trades) == len(trade_buy.x)
assert trade_buy.marker.color == 'cyan'
assert trade_buy.marker.symbol == 'circle-open'
assert trade_buy.text[0] == '4.0%, roi, 15 min'
assert trade_buy.text[0] == '3.99%, roi, 15 min'
trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit')
assert isinstance(trade_sell, go.Scatter)
@ -180,7 +179,7 @@ def test_plot_trades(testdatadir, caplog):
assert len(trades.loc[trades['profit_ratio'] > 0]) == len(trade_sell.x)
assert trade_sell.marker.color == 'green'
assert trade_sell.marker.symbol == 'square-open'
assert trade_sell.text[0] == '4.0%, roi, 15 min'
assert trade_sell.text[0] == '3.99%, roi, 15 min'
trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss')
assert isinstance(trade_sell_loss, go.Scatter)
@ -188,7 +187,7 @@ def test_plot_trades(testdatadir, caplog):
assert len(trades.loc[trades['profit_ratio'] <= 0]) == len(trade_sell_loss.x)
assert trade_sell_loss.marker.color == 'red'
assert trade_sell_loss.marker.symbol == 'square-open'
assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min'
assert trade_sell_loss.text[5] == '-10.45%, stop_loss, 720 min'
def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog):

View File

@ -185,17 +185,18 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
(100, 11, 500, 100),
(1000, 11, 500, 500), # Above max-stake
(20, 15, 10, 0), # Minimum stake > max-stake
(1, 11, 100, 11), # Below min stake
(9, 11, 100, 11), # Below min stake
(1, 15, 10, 0), # Below min stake and min_stake > max_stake
(20, 50, 100, 0), # Below min stake and stake * 1.3 > min_stake
])
def test__validate_stake_amount(mocker, default_conf,
stake_amount, min_stake_amount, max_stake_amount, expected):
def test_validate_stake_amount(mocker, default_conf,
stake_amount, min_stake_amount, max_stake_amount, expected):
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch("freqtrade.wallets.Wallets.get_available_stake_amount",
return_value=max_stake_amount)
res = freqtrade.wallets._validate_stake_amount('XRP/USDT', stake_amount, min_stake_amount)
res = freqtrade.wallets.validate_stake_amount('XRP/USDT', stake_amount, min_stake_amount)
assert res == expected