Merge branch 'develop' into pr/Axel-CH/8779

This commit is contained in:
Matthias
2023-09-07 20:19:25 +02:00
63 changed files with 1709 additions and 604 deletions
+4 -4
View File
@@ -25,7 +25,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-20.04, ubuntu-22.04 ]
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
@@ -127,7 +127,7 @@ jobs:
strategy:
matrix:
os: [ macos-latest ]
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
@@ -237,7 +237,7 @@ jobs:
strategy:
matrix:
os: [ windows-latest ]
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
@@ -448,7 +448,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
python-version: "3.11"
- name: Extract branch name
shell: bash
+1 -1
View File
@@ -8,7 +8,7 @@ repos:
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.5.0"
rev: "v1.5.1"
hooks:
- id: mypy
exclude: build_helpers
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.11.4-slim-bullseye as base
FROM python:3.11.5-slim-bullseye as base
# Setup env
ENV LANG C.UTF-8
+2 -2
View File
@@ -59,7 +59,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
## Features
- [x] **Based on Python 3.8+**: For botting on any operating system - Windows, macOS and Linux.
- [x] **Based on Python 3.9+**: For botting on any operating system - Windows, macOS and Linux.
- [x] **Persistence**: Persistence is achieved through sqlite.
- [x] **Dry-run**: Run the bot without paying money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
@@ -207,7 +207,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
### Software requirements
- [Python >= 3.8](http://docs.python-guide.org/en/latest/starting/installation/)
- [Python >= 3.9](http://docs.python-guide.org/en/latest/starting/installation/)
- [pip](https://pip.pypa.io/en/stable/installing/)
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
Binary file not shown.
+1
View File
@@ -613,6 +613,7 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo
* Orders are simulated, and will not be posted to the exchange.
* Market orders fill based on orderbook volume the moment the order is placed.
* Limit orders fill once the price reaches the defined level - or time out based on `unfilledtimeout` settings.
* Limit orders will be converted to market orders if they cross the price by more than 1%.
* In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled.
* Open orders (not trades, which are stored in the database) are kept open after bot restarts, with the assumption that they were not filled while being offline.
+4
View File
@@ -2,6 +2,10 @@
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
!!! Danger "Deprecated functionality"
`Edge positioning` (or short Edge) is currently in maintenance mode only (we keep existing functionality alive) and should be considered as deprecated.
It will currently not receive new features until either someone stepped forward to take up ownership of that module - or we'll decide to remove edge from freqtrade.
!!! Warning
When using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
+1 -2
View File
@@ -237,11 +237,10 @@ class MyCoolRLModel(ReinforcementLearner):
Reinforcement Learning models benefit from tracking training metrics. FreqAI has integrated Tensorboard to allow users to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command:
```bash
cd freqtrade
tensorboard --logdir user_data/models/unique-id
```
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell to view the output in their browser at 127.0.0.1:6006 (6006 is the default port used by Tensorboard).
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell to view the output in the browser at 127.0.0.1:6006 (6006 is the default port used by Tensorboard).
![tensorboard](assets/tensorboard.jpg)
+1 -1
View File
@@ -83,7 +83,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
Alternatively
- Python 3.8+
- Python 3.9+
- pip (pip3)
- git
- TA-Lib
+5 -5
View File
@@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
!!! Note
Python3.8 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
Python3.9 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
!!! Warning "Up-to-date clock"
@@ -42,7 +42,7 @@ These requirements apply to both [Script Installation](#script-installation) and
### Install guide
* [Python >= 3.8.x](http://docs.python-guide.org/en/latest/starting/installation/)
* [Python >= 3.9](http://docs.python-guide.org/en/latest/starting/installation/)
* [pip](https://pip.pypa.io/en/stable/installing/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
@@ -54,7 +54,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
!!! Note
Python3.8 or higher and the corresponding pip are assumed to be available.
Python3.9 or higher and the corresponding pip are assumed to be available.
=== "Debian/Ubuntu"
#### Install necessary dependencies
@@ -169,7 +169,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
** --install **
With this option, the script will install the bot and most dependencies:
You will need to have git and python3.8+ installed beforehand for this to work.
You will need to have git and python3.9+ installed beforehand for this to work.
* Mandatory software as: `ta-lib`
* Setup your virtualenv under `.venv/`
@@ -286,7 +286,7 @@ cd freqtrade
#### Freqtrade install: Conda Environment
```bash
conda create --name freqtrade python=3.10
conda create --name freqtrade python=3.11
```
!!! Note "Creating Conda Environment"
+2 -2
View File
@@ -1,6 +1,6 @@
markdown==3.4.4
mkdocs==1.5.2
mkdocs-material==9.2.1
mkdocs-material==9.2.7
mdx_truly_sane_lists==1.3
pymdown-extensions==10.1
pymdown-extensions==10.3
jinja2==3.1.2
+2
View File
@@ -151,6 +151,8 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `performance` | Show performance of each finished trade grouped by pair.
| `balance` | Show account balance per currency.
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
| `weekly <n>` | Shows profit or loss per week, over the last n days (n defaults to 4).
| `monthly <n>` | Shows profit or loss per month, over the last n days (n defaults to 3).
| `stats` | Display a summary of profit / loss reasons as well as average holding times.
| `whitelist` | Show the current whitelist.
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
+150 -27
View File
@@ -164,6 +164,31 @@ E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoplo
During backtesting, `current_rate` (and `current_profit`) are provided against the candle's high (or low for short trades) - while the resulting stoploss is evaluated against the candle's low (or high for short trades).
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss.
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
!!! Note "Use of dates"
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
!!! Tip "Trailing stoploss"
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
### Adjust stoploss after position adjustments
Depending on your strategy, you may encounter the need to adjust the stoploss in both directions after a [position adjustment](#adjust-trade-position).
For this, freqtrade will make an additional call with `after_fill=True` after an order fills, which will allow the strategy to move the stoploss in any direction (also widening the gap between stoploss and current price, which is otherwise forbidden).
!!! Note "backwards compatibility"
This call will only be made if the `after_fill` parameter is part of the function definition of your `custom_stoploss` function.
As such, this will not impact (and with that, surprise) existing, running strategies.
### Custom stoploss examples
The next section will show some examples on what's possible with the custom stoploss function.
Of course, many more things are possible, and all examples can be combined at will.
#### Trailing stop via custom stoploss
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
@@ -179,7 +204,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
"""
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate.
@@ -187,7 +213,7 @@ class AwesomeStrategy(IStrategy):
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns the initial stoploss value
When not implemented by a strategy, returns the initial stoploss value.
Only called when use_custom_stoploss is set to True.
:param pair: Pair that's currently analyzed
@@ -195,25 +221,13 @@ class AwesomeStrategy(IStrategy):
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param after_fill: True if the stoploss is called after the order was filled.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the current rate
:return float: New stoploss value, relative to the current_rate
"""
return -0.04
```
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
!!! Note "Use of dates"
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
!!! Tip "Trailing stoploss"
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
### Custom stoploss examples
The next section will show some examples on what's possible with the custom stoploss function.
Of course, many more things are possible, and all examples can be combined at will.
#### Time based trailing stop
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
@@ -229,14 +243,45 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
if current_time - timedelta(minutes=120) > trade.open_date_utc:
return -0.05
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
return -0.10
return 1
return None
```
#### Time based trailing stop with after-fill adjustments
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
If an additional order fills, set stoploss to -10% below the new `open_rate` ([Averaged across all entries](#position-adjust-calculations)).
``` python
from datetime import datetime, timedelta
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
if after_fill:
# After an additional order, start with a stoploss of 10% below the new open rate
return stoploss_from_open(0.10, current_profit, is_short=trade.is_short, leverage=trade.leverage)
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
if current_time - timedelta(minutes=120) > trade.open_date_utc:
return -0.05
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
return -0.10
return None
```
#### Different stoploss per pair
@@ -255,7 +300,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
if pair in ('ETH/BTC', 'XRP/BTC'):
return -0.10
@@ -281,7 +327,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
if current_profit < 0.04:
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
@@ -314,7 +361,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
# evaluate highest to lowest, so that highest possible stop is used
if current_profit > 0.40:
@@ -325,7 +373,7 @@ class AwesomeStrategy(IStrategy):
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
# return maximum stoploss value, keeping current stoploss price unchanged
return 1
return None
```
#### Custom stoploss using an indicator from dataframe example
@@ -342,7 +390,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
@@ -355,7 +404,7 @@ class AwesomeStrategy(IStrategy):
return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)
# return maximum stoploss value, keeping current stoploss price unchanged
return 1
return None
```
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
@@ -364,15 +413,89 @@ See [Dataframe access](strategy-advanced.md#dataframe-access) for more informati
#### Stoploss relative to open price
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
Stoploss values returned from `custom_stoploss()` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the _entry_ price instead.
`stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired trade profit above the entry point.
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`).
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
This function will consider leverage - so at 10x leverage, the actual stoploss would be 0.7% above $100 (0.7% * 10x = 7%).
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, stoploss_from_open
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
return 1
```
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
!!! Note
Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings.
This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade
is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `exit_reason` in
`confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when
`current_profit < open_relative_stop`.
#### Stoploss percentage from absolute price
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
The helper function `stoploss_from_absolute()` can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), current_rate, is_short=trade.is_short, leverage=trade.leverage)`.
For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, stoploss_from_absolute, timeframe_to_prev_date
class AwesomeStrategy(IStrategy):
use_custom_stoploss = True
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
return dataframe
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
candle = dataframe.iloc[-1].squeeze()
sign = 1 if trade.is_short else -1
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
current_rate, is_short=trade.is_short,
leverage=trade.leverage)
```
---
+61 -140
View File
@@ -586,6 +586,67 @@ for more information.
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
### *merge_informative_pair()*
This method helps you merge an informative pair to a regular dataframe without lookahead bias.
It's there to help you merge the dataframe in a safe and consistent way.
Options:
- Rename the columns for you to create unique columns
- Merge the dataframe without lookahead bias
- Forward-fill (optional)
For a full sample, please refer to the [complete data provider example](#complete-data-provider-sample) below.
All columns of the informative dataframe will be available on the returning dataframe in a renamed fashion:
!!! Example "Column renaming"
Assuming `inf_tf = '1d'` the resulting columns will be:
``` python
'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe
'date_1d', 'open_1d', 'high_1d', 'low_1d', 'close_1d', 'rsi_1d' # from the informative dataframe
```
??? Example "Column renaming - 1h"
Assuming `inf_tf = '1h'` the resulting columns will be:
``` python
'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe
'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe
```
??? Example "Custom implementation"
A custom implementation for this is possible, and can be done as follows:
``` python
# Shift date by 1 candle
# This is necessary since the data is always the "open date"
# and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00
minutes = timeframe_to_minutes(inf_tf)
# Only do this if the timeframes are different:
informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm')
# Rename columns to be unique
informative.columns = [f"{col}_{inf_tf}" for col in informative.columns]
# Assuming inf_tf = '1d' - then the columns will now be:
# date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d
# Combine the 2 dataframes
# all indicators on the informative sample MUST be calculated before this point
dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left')
# FFill to have the 1d value available in every row throughout the day.
# Without this, comparisons would only work once per day.
dataframe = dataframe.ffill()
```
!!! Warning "Informative timeframe < timeframe"
Using informative timeframes smaller than the dataframe timeframe is not recommended with this method, as it will not use any of the additional information this would provide.
To use the more detailed information properly, more advanced methods should be applied (which are out of scope for freqtrade documentation, as it'll depend on the respective need).
## Additional data (DataProvider)
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
@@ -810,146 +871,6 @@ class SampleStrategy(IStrategy):
***
## Helper functions
### *merge_informative_pair()*
This method helps you merge an informative pair to a regular dataframe without lookahead bias.
It's there to help you merge the dataframe in a safe and consistent way.
Options:
- Rename the columns for you to create unique columns
- Merge the dataframe without lookahead bias
- Forward-fill (optional)
For a full sample, please refer to the [complete data provider example](#complete-data-provider-sample) below.
All columns of the informative dataframe will be available on the returning dataframe in a renamed fashion:
!!! Example "Column renaming"
Assuming `inf_tf = '1d'` the resulting columns will be:
``` python
'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe
'date_1d', 'open_1d', 'high_1d', 'low_1d', 'close_1d', 'rsi_1d' # from the informative dataframe
```
??? Example "Column renaming - 1h"
Assuming `inf_tf = '1h'` the resulting columns will be:
``` python
'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe
'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe
```
??? Example "Custom implementation"
A custom implementation for this is possible, and can be done as follows:
``` python
# Shift date by 1 candle
# This is necessary since the data is always the "open date"
# and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00
minutes = timeframe_to_minutes(inf_tf)
# Only do this if the timeframes are different:
informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm')
# Rename columns to be unique
informative.columns = [f"{col}_{inf_tf}" for col in informative.columns]
# Assuming inf_tf = '1d' - then the columns will now be:
# date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d
# Combine the 2 dataframes
# all indicators on the informative sample MUST be calculated before this point
dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left')
# FFill to have the 1d value available in every row throughout the day.
# Without this, comparisons would only work once per day.
dataframe = dataframe.ffill()
```
!!! Warning "Informative timeframe < timeframe"
Using informative timeframes smaller than the dataframe timeframe is not recommended with this method, as it will not use any of the additional information this would provide.
To use the more detailed information properly, more advanced methods should be applied (which are out of scope for freqtrade documentation, as it'll depend on the respective need).
***
### *stoploss_from_open()*
Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the entry point instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired trade profit above the entry point.
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`).
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
This function will consider leverage - so at 10x leverage, the actual stoploss would be 0.7% above $100 (0.7% * 10x = 7%).
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, stoploss_from_open
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
return 1
```
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
!!! Note
Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings.
This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade
is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `exit_reason` in
`confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when
`current_profit < open_relative_stop`.
### *stoploss_from_absolute()*
In some situations it may be confusing to deal with stops relative to current rate. Instead, you may define a stoploss level using an absolute price.
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)`.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, stoploss_from_absolute
class AwesomeStrategy(IStrategy):
use_custom_stoploss = True
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
return dataframe
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
candle = dataframe.iloc[-1].squeeze()
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)
```
## Additional data (Wallets)
The strategy provides access to the `wallets` object. This contains the current balances on the exchange.
+3 -2
View File
@@ -311,12 +311,13 @@ After:
``` python hl_lines="5 7"
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short, leverage=trade.leverage)
```
+1 -1
View File
@@ -24,7 +24,7 @@ git clone https://github.com/freqtrade/freqtrade.git
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows.
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.9, 3.10 and 3.11) and for 64bit Windows.
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
Other versions must be downloaded from the above link.
+1 -1
View File
@@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = '2023.8.dev'
__version__ = '2023.9-dev'
if 'dev' in __version__:
from pathlib import Path
+1 -1
View File
@@ -3,7 +3,7 @@
__main__.py for Freqtrade
To launch Freqtrade as a module
> python -m freqtrade (with Python >= 3.8)
> python -m freqtrade (with Python >= 3.9)
"""
from freqtrade import main
+1
View File
@@ -688,6 +688,7 @@ CANCEL_REASON = {
"CANCELLED_ON_EXCHANGE": "cancelled on exchange",
"FORCE_EXIT": "forcesold",
"REPLACE": "cancelled to be replaced by new limit order",
"REPLACE_FAILED": "failed to replace order, deleting Trade",
"USER_CANCEL": "user requested order cancel"
}
+10 -2
View File
@@ -119,8 +119,15 @@ def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ):
new['avg_win'] = (new['profit_abs_wins'] / new.iloc[:, 1]).fillna(0)
new['avg_loss'] = (new['profit_abs_loss'] / new.iloc[:, 2]).fillna(0)
new.columns = ['total_num_buys', 'wins', 'losses', 'profit_abs_wins', 'profit_abs_loss',
'profit_tot', 'wl_ratio_pct', 'avg_win', 'avg_loss']
new['exp_ratio'] = (
(
(1 + (new['avg_win'] / abs(new['avg_loss']))) * (new['wl_ratio_pct'] / 100)
) - 1).fillna(0)
new.columns = ['total_num_buys', 'wins', 'losses',
'profit_abs_wins', 'profit_abs_loss',
'profit_tot', 'wl_ratio_pct',
'avg_win', 'avg_loss', 'exp_ratio']
sortcols = ['total_num_buys']
@@ -204,6 +211,7 @@ def prepare_results(analysed_trades, stratname,
timerange=None):
res_df = pd.DataFrame()
for pair, trades in analysed_trades[stratname].items():
trades.dropna(subset=['close_date'], inplace=True)
res_df = pd.concat([res_df, trades], ignore_index=True)
res_df = _select_rows_within_dates(res_df, timerange)
+478 -22
View File
@@ -6399,6 +6399,120 @@
}
}
],
"BTC/USDT:USDT-231229": [
{
"tier": 1.0,
"currency": "USDT",
"minNotional": 0.0,
"maxNotional": 375000.0,
"maintenanceMarginRate": 0.02,
"maxLeverage": 25.0,
"info": {
"bracket": "1",
"initialLeverage": "25",
"notionalCap": "375000",
"notionalFloor": "0",
"maintMarginRatio": "0.02",
"cum": "0.0"
}
},
{
"tier": 2.0,
"currency": "USDT",
"minNotional": 375000.0,
"maxNotional": 2000000.0,
"maintenanceMarginRate": 0.05,
"maxLeverage": 10.0,
"info": {
"bracket": "2",
"initialLeverage": "10",
"notionalCap": "2000000",
"notionalFloor": "375000",
"maintMarginRatio": "0.05",
"cum": "11250.0"
}
},
{
"tier": 3.0,
"currency": "USDT",
"minNotional": 2000000.0,
"maxNotional": 4000000.0,
"maintenanceMarginRate": 0.1,
"maxLeverage": 5.0,
"info": {
"bracket": "3",
"initialLeverage": "5",
"notionalCap": "4000000",
"notionalFloor": "2000000",
"maintMarginRatio": "0.1",
"cum": "111250.0"
}
},
{
"tier": 4.0,
"currency": "USDT",
"minNotional": 4000000.0,
"maxNotional": 10000000.0,
"maintenanceMarginRate": 0.125,
"maxLeverage": 4.0,
"info": {
"bracket": "4",
"initialLeverage": "4",
"notionalCap": "10000000",
"notionalFloor": "4000000",
"maintMarginRatio": "0.125",
"cum": "211250.0"
}
},
{
"tier": 5.0,
"currency": "USDT",
"minNotional": 10000000.0,
"maxNotional": 20000000.0,
"maintenanceMarginRate": 0.15,
"maxLeverage": 3.0,
"info": {
"bracket": "5",
"initialLeverage": "3",
"notionalCap": "20000000",
"notionalFloor": "10000000",
"maintMarginRatio": "0.15",
"cum": "461250.0"
}
},
{
"tier": 6.0,
"currency": "USDT",
"minNotional": 20000000.0,
"maxNotional": 40000000.0,
"maintenanceMarginRate": 0.25,
"maxLeverage": 2.0,
"info": {
"bracket": "6",
"initialLeverage": "2",
"notionalCap": "40000000",
"notionalFloor": "20000000",
"maintMarginRatio": "0.25",
"cum": "2461250.0"
}
},
{
"tier": 7.0,
"currency": "USDT",
"minNotional": 40000000.0,
"maxNotional": 120000000.0,
"maintenanceMarginRate": 0.5,
"maxLeverage": 1.0,
"info": {
"bracket": "7",
"initialLeverage": "1",
"notionalCap": "120000000",
"notionalFloor": "40000000",
"maintMarginRatio": "0.5",
"cum": "1.246125E7"
}
}
],
"BTCDOM/USDT:USDT": [
{
"tier": 1.0,
@@ -8487,6 +8601,120 @@
}
}
],
"CYBER/USDT:USDT": [
{
"tier": 1.0,
"currency": "USDT",
"minNotional": 0.0,
"maxNotional": 5000.0,
"maintenanceMarginRate": 0.02,
"maxLeverage": 20.0,
"info": {
"bracket": "1",
"initialLeverage": "20",
"notionalCap": "5000",
"notionalFloor": "0",
"maintMarginRatio": "0.02",
"cum": "0.0"
}
},
{
"tier": 2.0,
"currency": "USDT",
"minNotional": 5000.0,
"maxNotional": 25000.0,
"maintenanceMarginRate": 0.025,
"maxLeverage": 15.0,
"info": {
"bracket": "2",
"initialLeverage": "15",
"notionalCap": "25000",
"notionalFloor": "5000",
"maintMarginRatio": "0.025",
"cum": "25.0"
}
},
{
"tier": 3.0,
"currency": "USDT",
"minNotional": 25000.0,
"maxNotional": 200000.0,
"maintenanceMarginRate": 0.05,
"maxLeverage": 10.0,
"info": {
"bracket": "3",
"initialLeverage": "10",
"notionalCap": "200000",
"notionalFloor": "25000",
"maintMarginRatio": "0.05",
"cum": "650.0"
}
},
{
"tier": 4.0,
"currency": "USDT",
"minNotional": 200000.0,
"maxNotional": 500000.0,
"maintenanceMarginRate": 0.1,
"maxLeverage": 5.0,
"info": {
"bracket": "4",
"initialLeverage": "5",
"notionalCap": "500000",
"notionalFloor": "200000",
"maintMarginRatio": "0.1",
"cum": "10650.0"
}
},
{
"tier": 5.0,
"currency": "USDT",
"minNotional": 500000.0,
"maxNotional": 1000000.0,
"maintenanceMarginRate": 0.125,
"maxLeverage": 4.0,
"info": {
"bracket": "5",
"initialLeverage": "4",
"notionalCap": "1000000",
"notionalFloor": "500000",
"maintMarginRatio": "0.125",
"cum": "23150.0"
}
},
{
"tier": 6.0,
"currency": "USDT",
"minNotional": 1000000.0,
"maxNotional": 3000000.0,
"maintenanceMarginRate": 0.25,
"maxLeverage": 2.0,
"info": {
"bracket": "6",
"initialLeverage": "2",
"notionalCap": "3000000",
"notionalFloor": "1000000",
"maintMarginRatio": "0.25",
"cum": "148150.0"
}
},
{
"tier": 7.0,
"currency": "USDT",
"minNotional": 3000000.0,
"maxNotional": 5000000.0,
"maintenanceMarginRate": 0.5,
"maxLeverage": 1.0,
"info": {
"bracket": "7",
"initialLeverage": "1",
"notionalCap": "5000000",
"notionalFloor": "3000000",
"maintMarginRatio": "0.5",
"cum": "898150.0"
}
}
],
"DAR/USDT:USDT": [
{
"tier": 1.0,
@@ -9212,10 +9440,10 @@
"minNotional": 0.0,
"maxNotional": 100000.0,
"maintenanceMarginRate": 0.025,
"maxLeverage": 20.0,
"maxLeverage": 10.0,
"info": {
"bracket": "1",
"initialLeverage": "20",
"initialLeverage": "10",
"notionalCap": "100000",
"notionalFloor": "0",
"maintMarginRatio": "0.025",
@@ -9228,10 +9456,10 @@
"minNotional": 100000.0,
"maxNotional": 500000.0,
"maintenanceMarginRate": 0.05,
"maxLeverage": 10.0,
"maxLeverage": 8.0,
"info": {
"bracket": "2",
"initialLeverage": "10",
"initialLeverage": "8",
"notionalCap": "500000",
"notionalFloor": "100000",
"maintMarginRatio": "0.05",
@@ -9290,13 +9518,13 @@
"tier": 6.0,
"currency": "BUSD",
"minNotional": 5000000.0,
"maxNotional": 8000000.0,
"maxNotional": 5200000.0,
"maintenanceMarginRate": 0.5,
"maxLeverage": 1.0,
"info": {
"bracket": "6",
"initialLeverage": "1",
"notionalCap": "8000000",
"notionalCap": "5200000",
"notionalFloor": "5000000",
"maintMarginRatio": "0.5",
"cum": "1527500.0"
@@ -11447,6 +11675,120 @@
}
}
],
"ETH/USDT:USDT-231229": [
{
"tier": 1.0,
"currency": "USDT",
"minNotional": 0.0,
"maxNotional": 375000.0,
"maintenanceMarginRate": 0.02,
"maxLeverage": 25.0,
"info": {
"bracket": "1",
"initialLeverage": "25",
"notionalCap": "375000",
"notionalFloor": "0",
"maintMarginRatio": "0.02",
"cum": "0.0"
}
},
{
"tier": 2.0,
"currency": "USDT",
"minNotional": 375000.0,
"maxNotional": 2000000.0,
"maintenanceMarginRate": 0.05,
"maxLeverage": 10.0,
"info": {
"bracket": "2",
"initialLeverage": "10",
"notionalCap": "2000000",
"notionalFloor": "375000",
"maintMarginRatio": "0.05",
"cum": "11250.0"
}
},
{
"tier": 3.0,
"currency": "USDT",
"minNotional": 2000000.0,
"maxNotional": 4000000.0,
"maintenanceMarginRate": 0.1,
"maxLeverage": 5.0,
"info": {
"bracket": "3",
"initialLeverage": "5",
"notionalCap": "4000000",
"notionalFloor": "2000000",
"maintMarginRatio": "0.1",
"cum": "111250.0"
}
},
{
"tier": 4.0,
"currency": "USDT",
"minNotional": 4000000.0,
"maxNotional": 10000000.0,
"maintenanceMarginRate": 0.125,
"maxLeverage": 4.0,
"info": {
"bracket": "4",
"initialLeverage": "4",
"notionalCap": "10000000",
"notionalFloor": "4000000",
"maintMarginRatio": "0.125",
"cum": "211250.0"
}
},
{
"tier": 5.0,
"currency": "USDT",
"minNotional": 10000000.0,
"maxNotional": 20000000.0,
"maintenanceMarginRate": 0.15,
"maxLeverage": 3.0,
"info": {
"bracket": "5",
"initialLeverage": "3",
"notionalCap": "20000000",
"notionalFloor": "10000000",
"maintMarginRatio": "0.15",
"cum": "461250.0"
}
},
{
"tier": 6.0,
"currency": "USDT",
"minNotional": 20000000.0,
"maxNotional": 40000000.0,
"maintenanceMarginRate": 0.25,
"maxLeverage": 2.0,
"info": {
"bracket": "6",
"initialLeverage": "2",
"notionalCap": "40000000",
"notionalFloor": "20000000",
"maintMarginRatio": "0.25",
"cum": "2461250.0"
}
},
{
"tier": 7.0,
"currency": "USDT",
"minNotional": 40000000.0,
"maxNotional": 120000000.0,
"maintenanceMarginRate": 0.5,
"maxLeverage": 1.0,
"info": {
"bracket": "7",
"initialLeverage": "1",
"notionalCap": "120000000",
"notionalFloor": "40000000",
"maintMarginRatio": "0.5",
"cum": "1.246125E7"
}
}
],
"FET/USDT:USDT": [
{
"tier": 1.0,
@@ -17160,10 +17502,10 @@
"minNotional": 0.0,
"maxNotional": 5000.0,
"maintenanceMarginRate": 0.02,
"maxLeverage": 25.0,
"maxLeverage": 10.0,
"info": {
"bracket": "1",
"initialLeverage": "25",
"initialLeverage": "10",
"notionalCap": "5000",
"notionalFloor": "0",
"maintMarginRatio": "0.02",
@@ -17176,10 +17518,10 @@
"minNotional": 5000.0,
"maxNotional": 25000.0,
"maintenanceMarginRate": 0.025,
"maxLeverage": 15.0,
"maxLeverage": 8.0,
"info": {
"bracket": "2",
"initialLeverage": "15",
"initialLeverage": "8",
"notionalCap": "25000",
"notionalFloor": "5000",
"maintMarginRatio": "0.025",
@@ -17192,10 +17534,10 @@
"minNotional": 25000.0,
"maxNotional": 100000.0,
"maintenanceMarginRate": 0.05,
"maxLeverage": 10.0,
"maxLeverage": 6.0,
"info": {
"bracket": "3",
"initialLeverage": "10",
"initialLeverage": "6",
"notionalCap": "100000",
"notionalFloor": "25000",
"maintMarginRatio": "0.05",
@@ -17254,13 +17596,13 @@
"tier": 7.0,
"currency": "BUSD",
"minNotional": 3000000.0,
"maxNotional": 8000000.0,
"maxNotional": 3200000.0,
"maintenanceMarginRate": 0.5,
"maxLeverage": 1.0,
"info": {
"bracket": "7",
"initialLeverage": "1",
"notionalCap": "8000000",
"notionalCap": "3200000",
"notionalFloor": "3000000",
"maintMarginRatio": "0.5",
"cum": "949400.0"
@@ -22333,6 +22675,120 @@
}
}
],
"SEI/USDT:USDT": [
{
"tier": 1.0,
"currency": "USDT",
"minNotional": 0.0,
"maxNotional": 5000.0,
"maintenanceMarginRate": 0.02,
"maxLeverage": 20.0,
"info": {
"bracket": "1",
"initialLeverage": "20",
"notionalCap": "5000",
"notionalFloor": "0",
"maintMarginRatio": "0.02",
"cum": "0.0"
}
},
{
"tier": 2.0,
"currency": "USDT",
"minNotional": 5000.0,
"maxNotional": 25000.0,
"maintenanceMarginRate": 0.025,
"maxLeverage": 15.0,
"info": {
"bracket": "2",
"initialLeverage": "15",
"notionalCap": "25000",
"notionalFloor": "5000",
"maintMarginRatio": "0.025",
"cum": "25.0"
}
},
{
"tier": 3.0,
"currency": "USDT",
"minNotional": 25000.0,
"maxNotional": 200000.0,
"maintenanceMarginRate": 0.05,
"maxLeverage": 10.0,
"info": {
"bracket": "3",
"initialLeverage": "10",
"notionalCap": "200000",
"notionalFloor": "25000",
"maintMarginRatio": "0.05",
"cum": "650.0"
}
},
{
"tier": 4.0,
"currency": "USDT",
"minNotional": 200000.0,
"maxNotional": 500000.0,
"maintenanceMarginRate": 0.1,
"maxLeverage": 5.0,
"info": {
"bracket": "4",
"initialLeverage": "5",
"notionalCap": "500000",
"notionalFloor": "200000",
"maintMarginRatio": "0.1",
"cum": "10650.0"
}
},
{
"tier": 5.0,
"currency": "USDT",
"minNotional": 500000.0,
"maxNotional": 1000000.0,
"maintenanceMarginRate": 0.125,
"maxLeverage": 4.0,
"info": {
"bracket": "5",
"initialLeverage": "4",
"notionalCap": "1000000",
"notionalFloor": "500000",
"maintMarginRatio": "0.125",
"cum": "23150.0"
}
},
{
"tier": 6.0,
"currency": "USDT",
"minNotional": 1000000.0,
"maxNotional": 3000000.0,
"maintenanceMarginRate": 0.25,
"maxLeverage": 2.0,
"info": {
"bracket": "6",
"initialLeverage": "2",
"notionalCap": "3000000",
"notionalFloor": "1000000",
"maintMarginRatio": "0.25",
"cum": "148150.0"
}
},
{
"tier": 7.0,
"currency": "USDT",
"minNotional": 3000000.0,
"maxNotional": 5000000.0,
"maintenanceMarginRate": 0.5,
"maxLeverage": 1.0,
"info": {
"bracket": "7",
"initialLeverage": "1",
"notionalCap": "5000000",
"notionalFloor": "3000000",
"maintMarginRatio": "0.5",
"cum": "898150.0"
}
}
],
"SFP/USDT:USDT": [
{
"tier": 1.0,
@@ -22650,10 +23106,10 @@
"minNotional": 0.0,
"maxNotional": 50000.0,
"maintenanceMarginRate": 0.02,
"maxLeverage": 25.0,
"maxLeverage": 10.0,
"info": {
"bracket": "1",
"initialLeverage": "25",
"initialLeverage": "10",
"notionalCap": "50000",
"notionalFloor": "0",
"maintMarginRatio": "0.02",
@@ -22666,10 +23122,10 @@
"minNotional": 50000.0,
"maxNotional": 100000.0,
"maintenanceMarginRate": 0.025,
"maxLeverage": 20.0,
"maxLeverage": 8.0,
"info": {
"bracket": "2",
"initialLeverage": "20",
"initialLeverage": "8",
"notionalCap": "100000",
"notionalFloor": "50000",
"maintMarginRatio": "0.025",
@@ -22682,10 +23138,10 @@
"minNotional": 100000.0,
"maxNotional": 500000.0,
"maintenanceMarginRate": 0.05,
"maxLeverage": 10.0,
"maxLeverage": 6.0,
"info": {
"bracket": "3",
"initialLeverage": "10",
"initialLeverage": "6",
"notionalCap": "500000",
"notionalFloor": "100000",
"maintMarginRatio": "0.05",
@@ -22744,13 +23200,13 @@
"tier": 7.0,
"currency": "BUSD",
"minNotional": 5000000.0,
"maxNotional": 8000000.0,
"maxNotional": 5500000.0,
"maintenanceMarginRate": 0.5,
"maxLeverage": 1.0,
"info": {
"bracket": "7",
"initialLeverage": "1",
"notionalCap": "8000000",
"notionalCap": "5500000",
"notionalFloor": "5000000",
"maintMarginRatio": "0.5",
"cum": "1527750.0"
+30 -1
View File
@@ -1,6 +1,6 @@
""" Bybit exchange subclass """
import logging
from datetime import datetime
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple
import ccxt
@@ -11,6 +11,7 @@ from freqtrade.enums.candletype import CandleType
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.util.datetime_helpers import dt_now, dt_ts
logger = logging.getLogger(__name__)
@@ -203,3 +204,31 @@ class Bybit(Exchange):
return self._fetch_and_calculate_funding_fees(
pair, amount, is_short, open_date)
return 0.0
def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
"""
Fetch all orders for a pair "since"
:param pair: Pair for the query
:param since: Starting time for the query
"""
# On bybit, the distance between since and "until" can't exceed 7 days.
# we therefore need to split the query into multiple queries.
orders = []
while since < dt_now():
until = since + timedelta(days=7, minutes=-1)
orders += super().fetch_orders(pair, since, params={'until': dt_ts(until)})
since = until
return orders
def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
order = super().fetch_order(order_id, pair, params)
if (
order.get('status') == 'canceled'
and order.get('filled') == 0.0
and order.get('remaining') == 0.0
):
# Canceled orders will have "remaining=0" on bybit.
order['remaining'] = None
return order
+24 -21
View File
@@ -832,7 +832,7 @@ class Exchange:
rate: float, leverage: float, params: Dict = {},
stop_loss: bool = False) -> Dict[str, Any]:
now = dt_now()
order_id = f'dry_run_{side}_{now.timestamp()}'
order_id = f'dry_run_{side}_{pair}_{now.timestamp()}'
# Rounding here must respect to contract sizes
_amount = self._contracts_to_amount(
pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)))
@@ -863,8 +863,8 @@ class Exchange:
if self.exchange_has('fetchL2OrderBook'):
orderbook = self.fetch_l2_order_book(pair, 20)
if ordertype == "limit" and orderbook:
# Allow a 3% price difference
allowed_diff = 0.03
# Allow a 1% price difference
allowed_diff = 0.01
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
logger.info(
f"Converted order {pair} to market order due to price {rate} crossing spread "
@@ -920,7 +920,7 @@ class Exchange:
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
remaining_amount = amount
filled_amount = 0.0
filled_value = 0.0
book_entry_price = 0.0
for book_entry in orderbook[ob_type]:
book_entry_price = book_entry[0]
@@ -928,17 +928,17 @@ class Exchange:
if remaining_amount > 0:
if remaining_amount < book_entry_coin_volume:
# Orderbook at this slot bigger than remaining amount
filled_amount += remaining_amount * book_entry_price
filled_value += remaining_amount * book_entry_price
break
else:
filled_amount += book_entry_coin_volume * book_entry_price
filled_value += book_entry_coin_volume * book_entry_price
remaining_amount -= book_entry_coin_volume
else:
break
else:
# If remaining_amount wasn't consumed completely (break was not called)
filled_amount += remaining_amount * book_entry_price
forecast_avg_filled_price = max(filled_amount, 0) / amount
filled_value += remaining_amount * book_entry_price
forecast_avg_filled_price = max(filled_value, 0) / amount
# Limit max. slippage to specified value
if side == 'buy':
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
@@ -1421,8 +1421,17 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
def __fetch_orders_emulate(self, pair: str, since_ms: int) -> List[Dict]:
orders = []
if self.exchange_has('fetchClosedOrders'):
orders = self._api.fetch_closed_orders(pair, since=since_ms)
if self.exchange_has('fetchOpenOrders'):
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
orders.extend(orders_open)
return orders
@retrier(retries=0)
def fetch_orders(self, pair: str, since: datetime) -> List[Dict]:
def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
"""
Fetch all orders for a pair "since"
:param pair: Pair for the query
@@ -1431,26 +1440,20 @@ class Exchange:
if self._config['dry_run']:
return []
def fetch_orders_emulate() -> List[Dict]:
orders = []
if self.exchange_has('fetchClosedOrders'):
orders = self._api.fetch_closed_orders(pair, since=since_ms)
if self.exchange_has('fetchOpenOrders'):
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
orders.extend(orders_open)
return orders
try:
since_ms = int((since.timestamp() - 10) * 1000)
if self.exchange_has('fetchOrders'):
if not params:
params = {}
try:
orders: List[Dict] = self._api.fetch_orders(pair, since=since_ms)
orders: List[Dict] = self._api.fetch_orders(pair, since=since_ms, params=params)
except ccxt.NotSupported:
# Some exchanges don't support fetchOrders
# attempt to fetch open and closed orders separately
orders = fetch_orders_emulate()
orders = self.__fetch_orders_emulate(pair, since_ms)
else:
orders = fetch_orders_emulate()
orders = self.__fetch_orders_emulate(pair, since_ms)
self._log_exchange_response('fetch_orders', orders)
orders = [self._order_contracts_to_amount(o) for o in orders]
return orders
+54 -10
View File
@@ -248,6 +248,39 @@ def amount_to_contract_precision(
return amount
def __price_to_precision_significant_digits(
price: float,
price_precision: float,
*,
rounding_mode: int = ROUND,
) -> float:
"""
Implementation of ROUND_UP/Round_down for significant digits mode.
"""
from decimal import ROUND_DOWN as dec_ROUND_DOWN
from decimal import ROUND_UP as dec_ROUND_UP
from decimal import Decimal
dec = Decimal(str(price))
string = f'{dec:f}'
precision = round(price_precision)
q = precision - dec.adjusted() - 1
sigfig = Decimal('10') ** -q
if q < 0:
string_to_precision = string[:precision]
# string_to_precision is '' when we have zero precision
below = sigfig * Decimal(string_to_precision if string_to_precision else '0')
above = below + sigfig
res = above if rounding_mode == ROUND_UP else below
precise = f'{res:f}'
else:
precise = '{:f}'.format(dec.quantize(
sigfig,
rounding=dec_ROUND_DOWN if rounding_mode == ROUND_DOWN else dec_ROUND_UP)
)
return float(precise)
def price_to_precision(
price: float,
price_precision: Optional[float],
@@ -271,28 +304,39 @@ def price_to_precision(
:return: price rounded up to the precision the Exchange accepts
"""
if price_precision is not None and precisionMode is not None:
if rounding_mode not in (ROUND_UP, ROUND_DOWN):
# Use CCXT code where possible.
return float(decimal_to_precision(price, rounding_mode=rounding_mode,
precision=price_precision,
counting_mode=precisionMode
))
if precisionMode == TICK_SIZE:
if rounding_mode == ROUND:
ticks = price / price_precision
rounded_ticks = round(ticks)
return rounded_ticks * price_precision
precision = FtPrecise(price_precision)
price_str = FtPrecise(price)
missing = price_str % precision
if not missing == FtPrecise("0"):
return round(float(str(price_str - missing + precision)), 14)
if rounding_mode == ROUND_UP:
res = price_str - missing + precision
elif rounding_mode == ROUND_DOWN:
res = price_str - missing
return round(float(str(res)), 14)
return price
elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES):
elif precisionMode == DECIMAL_PLACES:
ndigits = round(price_precision)
if rounding_mode == ROUND:
return round(price, ndigits)
ticks = price * (10**ndigits)
if rounding_mode == ROUND_UP:
return ceil(ticks) / (10**ndigits)
if rounding_mode == TRUNCATE:
return int(ticks) / (10**ndigits)
if rounding_mode == ROUND_DOWN:
return floor(ticks) / (10**ndigits)
raise ValueError(f"Unknown rounding_mode {rounding_mode}")
elif precisionMode == SIGNIFICANT_DIGITS:
if rounding_mode in (ROUND_UP, ROUND_DOWN):
return __price_to_precision_significant_digits(
price, price_precision, rounding_mode=rounding_mode
)
raise ValueError(f"Unknown precisionMode {precisionMode}")
return price
+5 -2
View File
@@ -11,6 +11,8 @@ from gymnasium import spaces
from gymnasium.utils import seeding
from pandas import DataFrame
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
@@ -80,8 +82,9 @@ class BaseEnvironment(gym.Env):
self.can_short: bool = can_short
self.live: bool = live
if not self.live and self.add_state_info:
self.add_state_info = False
logger.warning("add_state_info is not available in backtesting. Deactivating.")
raise OperationalException("`add_state_info` is not available in backtesting. Change "
"parameter to false in your rl_config. See `add_state_info` "
"docs for more info.")
self.seed(seed)
self.reset_env(df, prices, window_size, reward_kwargs, starting_point)
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
torch.multiprocessing.set_sharing_strategy('file_system')
SB3_MODELS = ['PPO', 'A2C', 'DQN']
SB3_CONTRIB_MODELS = ['TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO']
SB3_CONTRIB_MODELS = ['TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'QRDQN']
class BaseReinforcementLearningModel(IFreqaiModel):
+44 -27
View File
@@ -781,7 +781,7 @@ class FreqtradeBot(LoggingMixin):
order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested)
order_id = order['id']
order_status = order.get('status')
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
logger.info(f"Order {order_id} was created for {pair} and status is {order_status}.")
# we assume the order is executed at the price requested
enter_limit_filled_price = enter_limit_requested
@@ -1410,7 +1410,7 @@ class FreqtradeBot(LoggingMixin):
replacing=replacing)
if adjusted_entry_price:
# place new order only if new price is supplied
self.execute_entry(
if not self.execute_entry(
pair=trade.pair,
stake_amount=(
order_obj.safe_remaining * order_obj.safe_price / trade.leverage),
@@ -1418,7 +1418,15 @@ class FreqtradeBot(LoggingMixin):
trade=trade,
is_short=trade.is_short,
order_adjust=True,
)
):
logger.warning(f"Could not replace order for {trade}.")
if trade.nr_of_successful_entries == 0:
# this is the first entry and we didn't get filled yet, delete trade
logger.warning(f"Removing {trade} from database.")
self._notify_enter_cancel(
trade, order_type=self.strategy.order_types['entry'],
reason=constants.CANCEL_REASON['REPLACE_FAILED'])
trade.delete()
def cancel_all_open_orders(self) -> None:
"""
@@ -1524,17 +1532,18 @@ class FreqtradeBot(LoggingMixin):
cancelled = False
# Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
filled_val: float = order.get('filled', 0.0) or 0.0
filled_rem_stake = trade.stake_amount - filled_val * trade.open_rate
filled_amt: float = order.get('filled', 0.0) or 0.0
# Filled val is in quote currency (after leverage)
filled_rem_stake = trade.stake_amount - (filled_amt * trade.open_rate / trade.leverage)
minstake = self.exchange.get_min_pair_stake_amount(
trade.pair, trade.open_rate, self.strategy.stoploss)
# Double-check remaining amount
if filled_val > 0:
if filled_amt > 0:
reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
if minstake and filled_rem_stake < minstake:
logger.warning(
f"Order {order_id} for {trade.pair} not cancelled, as "
f"the filled amount of {filled_val} would result in an unexitable trade.")
f"the filled amount of {filled_amt} would result in an unexitable trade.")
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
self._notify_exit_cancel(
@@ -1724,14 +1733,12 @@ class FreqtradeBot(LoggingMixin):
amount = order.safe_filled if fill else order.safe_amount
order_rate: float = order.safe_price
profit = trade.calc_profit(rate=order_rate, amount=amount, open_rate=trade.open_rate)
profit_ratio = trade.calc_profit_ratio(order_rate, amount, trade.open_rate)
profit = trade.calculate_profit(order_rate, amount, trade.open_rate)
else:
order_rate = trade.safe_close_rate
profit = trade.calc_profit(rate=order_rate) + (0.0 if fill else trade.realized_profit)
profit_ratio = trade.calc_profit_ratio(order_rate)
profit = trade.calculate_profit(rate=order_rate)
amount = trade.amount
gain = "profit" if profit_ratio > 0 else "loss"
gain = "profit" if profit.profit_ratio > 0 else "loss"
msg: RPCSellMsg = {
'type': (RPCMessageType.EXIT_FILL if fill
@@ -1749,8 +1756,8 @@ class FreqtradeBot(LoggingMixin):
'open_rate': trade.open_rate,
'close_rate': order_rate,
'current_rate': current_rate,
'profit_amount': profit,
'profit_ratio': profit_ratio,
'profit_amount': profit.profit_abs if fill else profit.total_profit,
'profit_ratio': profit.profit_ratio,
'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag,
'sell_reason': trade.exit_reason, # Deprecated
@@ -1782,11 +1789,10 @@ class FreqtradeBot(LoggingMixin):
order = self.order_obj_or_raise(order_id, order_or_none)
profit_rate: float = trade.safe_close_rate
profit_trade = trade.calc_profit(rate=profit_rate)
profit = trade.calculate_profit(rate=profit_rate)
current_rate = self.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss"
gain = "profit" if profit.profit_ratio > 0 else "loss"
msg: RPCSellCancelMsg = {
'type': RPCMessageType.EXIT_CANCEL,
@@ -1801,8 +1807,8 @@ class FreqtradeBot(LoggingMixin):
'amount': order.safe_amount_after_fee,
'open_rate': trade.open_rate,
'current_rate': current_rate,
'profit_amount': profit_trade,
'profit_ratio': profit_ratio,
'profit_amount': profit.profit_abs,
'profit_ratio': profit.profit_ratio,
'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag,
'sell_reason': trade.exit_reason, # Deprecated
@@ -1871,15 +1877,23 @@ class FreqtradeBot(LoggingMixin):
trade.update_trade(order_obj)
if order.get('status') in constants.NON_OPEN_EXCHANGE_STATES:
trade = self._update_trade_after_fill(trade, order_obj)
Trade.commit()
self.order_close_notify(trade, order_obj, stoploss_order, send_msg)
return False
def _update_trade_after_fill(self, trade: Trade, order: Order) -> Trade:
if order.status in constants.NON_OPEN_EXCHANGE_STATES:
# If a entry order was closed, force update on stoploss on exchange
if order.get('side') == trade.entry_side:
if order.ft_order_side == trade.entry_side:
trade = self.cancel_stoploss_on_exchange(trade)
if not self.edge:
# TODO: should shorting/leverage be supported by Edge,
# then this will need to be fixed.
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
if order.get('side') == trade.entry_side or (trade.amount > 0 and trade.is_open):
if order.ft_order_side == trade.entry_side or (trade.amount > 0 and trade.is_open):
# Must also run for partial exits
# TODO: Margin will need to use interest_rate as well.
# interest_rate = self.exchange.get_interest_rate()
@@ -1895,13 +1909,16 @@ class FreqtradeBot(LoggingMixin):
))
except DependencyException:
logger.warning('Unable to calculate liquidation price')
if self.strategy.use_custom_stoploss:
current_rate = self.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=True)
profit = trade.calc_profit_ratio(current_rate)
self.strategy.ft_stoploss_adjust(current_rate, trade,
datetime.now(timezone.utc), profit, 0,
after_fill=True)
# Updating wallets when order is closed
self.wallets.update()
Trade.commit()
self.order_close_notify(trade, order_obj, stoploss_order, send_msg)
return False
return trade
def order_close_notify(
self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
+8
View File
@@ -579,6 +579,11 @@ class Backtesting:
""" Rate is within candle, therefore filled"""
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
def _call_adjust_stop(self, current_date: datetime, trade: LocalTrade, current_rate: float):
profit = trade.calc_profit_ratio(current_rate)
self.strategy.ft_stoploss_adjust(current_rate, trade, # type: ignore
current_date, profit, 0, after_fill=True)
def _try_close_open_order(
self, order: Optional[Order], trade: LocalTrade, current_date: datetime,
row: Tuple) -> bool:
@@ -588,6 +593,9 @@ class Backtesting:
"""
if order and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_date, trade)
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
self._call_adjust_stop(current_date, trade, order.ft_price)
# pass
return True
return False
+7 -3
View File
@@ -88,6 +88,9 @@ def migrate_trades_and_orders_table(
stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null')
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null')
is_stop_loss_trailing = get_column_def(
cols, 'is_stop_loss_trailing',
f'coalesce({stop_loss_pct}, 0.0) <> coalesce({initial_stop_loss_pct}, 0.0)')
stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
max_rate = get_column_def(cols, 'max_rate', '0.0')
@@ -156,7 +159,7 @@ def migrate_trades_and_orders_table(
open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, amount_requested, open_date, close_date,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update,
is_stop_loss_trailing, stoploss_order_id, stoploss_last_update,
max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short,
@@ -175,6 +178,7 @@ def migrate_trades_and_orders_table(
{stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct,
{initial_stop_loss} initial_stop_loss,
{initial_stop_loss_pct} initial_stop_loss_pct,
{is_stop_loss_trailing} is_stop_loss_trailing,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate,
case when {exit_reason} = 'sell_signal' then 'exit_signal'
@@ -324,8 +328,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# if ('orders' not in previous_tables
# or not has_column(cols_orders, 'funding_fee')):
migrating = False
# if not has_column(cols_trades, 'max_stake_amount'):
if not has_column(cols_orders, 'ft_price'):
# if not has_column(cols_orders, 'ft_price'):
if not has_column(cols_trades, 'is_stop_loss_trailing'):
migrating = True
logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}")
+74 -19
View File
@@ -3,6 +3,7 @@ This module contains the class to persist trades into SQLite
"""
import logging
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from math import isclose
from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast
@@ -26,6 +27,14 @@ from freqtrade.util import FtPrecise, dt_now
logger = logging.getLogger(__name__)
@dataclass
class ProfitStruct:
profit_abs: float
profit_ratio: float
total_profit: float
total_profit_ratio: float
class Order(ModelBase):
"""
Order database model
@@ -240,7 +249,10 @@ class Order(ModelBase):
if (self.ft_order_side == trade.entry_side and self.price):
trade.open_rate = self.price
trade.recalc_trade_from_orders()
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True)
if trade.nr_of_successful_entries == 1:
trade.initial_stop_loss_pct = None
trade.is_stop_loss_trailing = False
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct)
@staticmethod
def update_orders(orders: List['Order'], order: Dict[str, Any]):
@@ -348,6 +360,7 @@ class LocalTrade:
initial_stop_loss: Optional[float] = 0.0
# percentage value of the initial stop loss
initial_stop_loss_pct: Optional[float] = None
is_stop_loss_trailing: bool = False
# stoploss order id which is on exchange
stoploss_order_id: Optional[str] = None
# last update time of the stoploss order on exchange
@@ -655,18 +668,18 @@ class LocalTrade:
self.stop_loss_pct = -1 * abs(percent)
def adjust_stop_loss(self, current_price: float, stoploss: Optional[float],
initial: bool = False, refresh: bool = False) -> None:
initial: bool = False, allow_refresh: bool = False) -> None:
"""
This adjusts the stop loss to it's most recently observed setting
:param current_price: Current rate the asset is traded
:param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price).
:param initial: Called to initiate stop_loss.
Skips everything if self.stop_loss is already set.
:param refresh: Called to refresh stop_loss, allows adjustment in both directions
"""
if stoploss is None or (initial and not (self.stop_loss is None or self.stop_loss == 0)):
# Don't modify if called with initial and nothing to do
return
refresh = True if refresh and self.nr_of_successful_entries == 1 else False
leverage = self.leverage or 1.0
if self.is_short:
@@ -677,7 +690,7 @@ class LocalTrade:
stop_loss_norm = price_to_precision(new_loss, self.price_precision, self.precision_mode,
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
# no stop loss assigned yet
if self.initial_stop_loss_pct is None or refresh:
if self.initial_stop_loss_pct is None:
self.__set_stop_loss(stop_loss_norm, stoploss)
self.initial_stop_loss = price_to_precision(
stop_loss_norm, self.price_precision, self.precision_mode,
@@ -692,8 +705,14 @@ class LocalTrade:
# stop losses only walk up, never down!,
# ? But adding more to a leveraged trade would create a lower liquidation price,
# ? decreasing the minimum stoploss
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
if (
allow_refresh
or (higher_stop and not self.is_short)
or (lower_stop and self.is_short)
):
logger.debug(f"{self.pair} - Adjusting stoploss...")
if not allow_refresh:
self.is_stop_loss_trailing = True
self.__set_stop_loss(stop_loss_norm, stoploss)
else:
logger.debug(f"{self.pair} - Keeping current stoploss...")
@@ -900,11 +919,26 @@ class LocalTrade:
open_rate: Optional[float] = None) -> float:
"""
Calculate the absolute profit in stake currency between Close and Open trade
Deprecated - only available for backwards compatibility
:param rate: close rate to compare with.
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
:return: profit in stake currency as float
"""
prof = self.calculate_profit(rate, amount, open_rate)
return prof.profit_abs
def calculate_profit(self, rate: float, amount: Optional[float] = None,
open_rate: Optional[float] = None) -> ProfitStruct:
"""
Calculate profit metrics (absolute, ratio, total, total ratio).
All calculations include fees.
:param rate: close rate to compare with.
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
:return: Profit structure, containing absolute and relative profits.
"""
close_trade_value = self.calc_close_trade_value(rate, amount)
if amount is None or open_rate is None:
open_trade_value = self.open_trade_value
@@ -912,10 +946,33 @@ class LocalTrade:
open_trade_value = self._calc_open_trade_value(amount, open_rate)
if self.is_short:
profit = open_trade_value - close_trade_value
profit_abs = open_trade_value - close_trade_value
else:
profit = close_trade_value - open_trade_value
return float(f"{profit:.8f}")
profit_abs = close_trade_value - open_trade_value
try:
if self.is_short:
profit_ratio = (1 - (close_trade_value / open_trade_value)) * self.leverage
else:
profit_ratio = ((close_trade_value / open_trade_value) - 1) * self.leverage
profit_ratio = float(f"{profit_ratio:.8f}")
except ZeroDivisionError:
profit_ratio = 0.0
total_profit_abs = profit_abs + self.realized_profit
total_profit_ratio = (
(total_profit_abs / self.max_stake_amount) * self.leverage
if self.max_stake_amount else 0.0
)
total_profit_ratio = float(f"{total_profit_ratio:.8f}")
profit_abs = float(f"{profit_abs:.8f}")
return ProfitStruct(
profit_abs=profit_abs,
profit_ratio=profit_ratio,
total_profit=profit_abs + self.realized_profit,
total_profit_ratio=total_profit_ratio,
)
def calc_profit_ratio(
self, rate: float, amount: Optional[float] = None,
@@ -936,15 +993,14 @@ class LocalTrade:
short_close_zero = (self.is_short and close_trade_value == 0.0)
long_close_zero = (not self.is_short and open_trade_value == 0.0)
leverage = self.leverage or 1.0
if (short_close_zero or long_close_zero):
return 0.0
else:
if self.is_short:
profit_ratio = (1 - (close_trade_value / open_trade_value)) * leverage
profit_ratio = (1 - (close_trade_value / open_trade_value)) * self.leverage
else:
profit_ratio = ((close_trade_value / open_trade_value) - 1) * leverage
profit_ratio = ((close_trade_value / open_trade_value) - 1) * self.leverage
return float(f"{profit_ratio:.8f}")
@@ -957,7 +1013,6 @@ class LocalTrade:
avg_price = FtPrecise(0.0)
close_profit = 0.0
close_profit_abs = 0.0
profit = None
# Reset funding fees
self.funding_fees = 0.0
funding_fees = 0.0
@@ -987,11 +1042,9 @@ class LocalTrade:
exit_rate = o.safe_price
exit_amount = o.safe_amount_after_fee
profit = self.calc_profit(rate=exit_rate, amount=exit_amount,
open_rate=float(avg_price))
close_profit_abs += profit
close_profit = self.calc_profit_ratio(
exit_rate, amount=exit_amount, open_rate=avg_price)
prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price))
close_profit_abs += prof.profit_abs
close_profit = prof.profit_ratio
else:
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
max_stake_amount += (tmp_amount * price)
@@ -1001,7 +1054,7 @@ class LocalTrade:
if close_profit:
self.close_profit = close_profit
self.realized_profit = close_profit_abs
self.close_profit_abs = profit
self.close_profit_abs = prof.profit_abs
current_amount_tr = amount_to_contract_precision(
float(current_amount), self.amount_precision, self.precision_mode, self.contract_size)
@@ -1226,7 +1279,7 @@ class LocalTrade:
logger.info(f"Found open trade: {trade}")
# skip case if trailing-stop changed the stoploss already.
if (trade.stop_loss == trade.initial_stop_loss
if (not trade.is_stop_loss_trailing
and trade.initial_stop_loss_pct != desired_stoploss):
# Stoploss value got changed
@@ -1298,6 +1351,8 @@ class Trade(ModelBase, LocalTrade):
# percentage value of the initial stop loss
initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True) # type: ignore
is_stop_loss_trailing: Mapped[bool] = mapped_column(
nullable=False, default=False) # type: ignore
# stoploss order id which is on exchange
stoploss_order_id: Mapped[Optional[str]] = mapped_column(
String(255), nullable=True, index=True) # type: ignore
@@ -260,6 +260,7 @@ class VolumePairList(IPairList):
quoteVolume = (pair_candles['quoteVolume']
.rolling(self._lookback_period)
.sum()
.fillna(0)
.iloc[-1])
# replace quoteVolume with range quoteVolume sum calculated above
@@ -29,9 +29,8 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
except re.error as err:
raise ValueError(f"Wildcard error in {pair_wc}, {err}")
for element in result:
if not re.fullmatch(r'^[A-Za-z0-9/-]+$', element):
result.remove(element)
result = [element for element in result if re.fullmatch(r'^[A-Za-z0-9:/-]+$', element)]
else:
for pair_wc in wildcardpl:
try:
+6
View File
@@ -218,6 +218,12 @@ class StrategyResolver(IResolver):
"Please update your strategy to implement "
"`populate_indicators`, `populate_entry_trend` and `populate_exit_trend` "
"with the metadata argument. ")
has_after_fill = ('after_fill' in getfullargspec(strategy.custom_stoploss).args
and check_override(strategy, IStrategy, 'custom_stoploss'))
if has_after_fill:
strategy._ft_stop_uses_after_fill = True
return strategy
@staticmethod
+7 -3
View File
@@ -141,6 +141,10 @@ class Profit(BaseModel):
expectancy_ratio: float
max_drawdown: float
max_drawdown_abs: float
max_drawdown_start: str
max_drawdown_start_timestamp: int
max_drawdown_end: str
max_drawdown_end_timestamp: int
trading_volume: Optional[float] = None
bot_start_timestamp: int
bot_start_date: str
@@ -157,7 +161,7 @@ class Stats(BaseModel):
durations: Dict[str, Optional[float]]
class DailyRecord(BaseModel):
class DailyWeeklyMonthlyRecord(BaseModel):
date: date
abs_profit: float
rel_profit: float
@@ -166,8 +170,8 @@ class DailyRecord(BaseModel):
trade_count: int
class Daily(BaseModel):
data: List[DailyRecord]
class DailyWeeklyMonthly(BaseModel):
data: List[DailyWeeklyMonthlyRecord]
fiat_display_currency: str
stake_currency: str
+16 -3
View File
@@ -11,7 +11,7 @@ from freqtrade.enums import CandleType, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily,
BlacklistResponse, Count, DailyWeeklyMonthly,
DeleteLockRequest, DeleteTrade,
ExchangeListResponse, ForceEnterPayload,
ForceEnterResponse, ForceExitPayload,
@@ -51,7 +51,8 @@ logger = logging.getLogger(__name__)
# 2.30: new /pairlists endpoint
# 2.31: new /backtest/history/ delete endpoint
# 2.32: new /backtest/history/ patch endpoint
API_VERSION = 2.32
# 2.33: Additional weekly/monthly metrics
API_VERSION = 2.33
# Public API, requires no auth.
router_public = APIRouter()
@@ -99,12 +100,24 @@ def stats(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_stats()
@router.get('/daily', response_model=Daily, tags=['info'])
@router.get('/daily', response_model=DailyWeeklyMonthly, tags=['info'])
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
return rpc._rpc_timeunit_profit(timescale, config['stake_currency'],
config.get('fiat_display_currency', ''))
@router.get('/weekly', response_model=DailyWeeklyMonthly, tags=['info'])
def weekly(timescale: int = 4, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
return rpc._rpc_timeunit_profit(timescale, config['stake_currency'],
config.get('fiat_display_currency', ''), 'weeks')
@router.get('/monthly', response_model=DailyWeeklyMonthly, tags=['info'])
def monthly(timescale: int = 3, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
return rpc._rpc_timeunit_profit(timescale, config['stake_currency'],
config.get('fiat_display_currency', ''), 'months')
@router.get('/status', response_model=List[OpenTradeSchema], tags=['info'])
def status(rpc: RPC = Depends(get_rpc)):
try:
+49 -31
View File
@@ -16,7 +16,7 @@ from sqlalchemy import func, select
from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config
from freqtrade.constants import CANCEL_REASON, Config
from freqtrade.data.history import load_data
from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection,
@@ -31,7 +31,7 @@ from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.rpc.rpc_types import RPCSendMsg
from freqtrade.util import dt_humanize, dt_now, shorten_date
from freqtrade.util import dt_humanize, dt_now, dt_ts_def, format_date, shorten_date
from freqtrade.wallets import PositionWallet, Wallet
@@ -183,6 +183,8 @@ class RPC:
]
oo_details = ''.join(map(str, oo_details_lst))
total_profit_abs = 0.0
total_profit_ratio: Optional[float] = None
# calculate profit and send message to user
if trade.is_open:
try:
@@ -191,23 +193,22 @@ class RPC:
except (ExchangeError, PricingError):
current_rate = NAN
if len(trade.select_filled_orders(trade.entry_side)) > 0:
current_profit = trade.calc_profit_ratio(
current_rate) if not isnan(current_rate) else NAN
current_profit_abs = trade.calc_profit(
current_rate) if not isnan(current_rate) else NAN
current_profit = current_profit_abs = current_profit_fiat = NAN
if not isnan(current_rate):
prof = trade.calculate_profit(current_rate)
current_profit = prof.profit_ratio
current_profit_abs = prof.profit_abs
total_profit_abs = prof.total_profit
total_profit_ratio = prof.total_profit_ratio
else:
current_profit = current_profit_abs = current_profit_fiat = 0.0
else:
# Closed trade ...
current_rate = trade.close_rate
current_profit = trade.close_profit or 0.0
current_profit_abs = trade.close_profit_abs or 0.0
total_profit_abs = trade.realized_profit + current_profit_abs
total_profit_ratio: Optional[float] = None
if trade.max_stake_amount:
total_profit_ratio = (
(total_profit_abs / trade.max_stake_amount) * trade.leverage
)
# Calculate fiat profit
if not isnan(current_profit_abs) and self._fiat_converter:
@@ -223,8 +224,11 @@ class RPC:
)
# Calculate guaranteed profit (in case of trailing stop)
stoploss_entry_dist = trade.calc_profit(trade.stop_loss)
stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss)
stop_entry = trade.calculate_profit(trade.stop_loss)
stoploss_entry_dist = stop_entry.profit_abs
stoploss_entry_dist_ratio = stop_entry.profit_ratio
# calculate distance to stoploss
stoploss_current_dist = trade.stop_loss - current_rate
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
@@ -270,8 +274,9 @@ class RPC:
profit_str = f'{NAN:.2%}'
else:
if trade.nr_of_successful_entries > 0:
trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
profit = trade.calculate_profit(current_rate)
trade_profit = profit.profit_abs
profit_str = f'{profit.profit_ratio:.2%}'
else:
trade_profit = 0.0
profit_str = f'{0.0:.2f}'
@@ -371,7 +376,7 @@ class RPC:
data = [
{
'date': f"{key.year}-{key.month:02d}" if timeunit == 'months' else key,
'date': key,
'abs_profit': value["amount"],
'starting_balance': value["daily_stake"],
'rel_profit': value["rel_profit"],
@@ -494,9 +499,10 @@ class RPC:
profit_ratio = NAN
profit_abs = NAN
else:
profit_ratio = trade.calc_profit_ratio(rate=current_rate)
profit_abs = trade.calc_profit(
rate=trade.close_rate or current_rate) + trade.realized_profit
profit = trade.calculate_profit(trade.close_rate or current_rate)
profit_ratio = profit.profit_ratio
profit_abs = profit.total_profit
profit_all_coin.append(profit_abs)
profit_all_ratio.append(profit_ratio)
@@ -532,7 +538,8 @@ class RPC:
winrate = (winning_trades / closed_trade_count) if closed_trade_count > 0 else 0
trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
trades_df = DataFrame([{'close_date': format_date(trade.close_date),
'close_date_dt': trade.close_date,
'profit_abs': trade.close_profit_abs}
for trade in trades if not trade.is_open and trade.close_date])
@@ -540,10 +547,15 @@ class RPC:
max_drawdown_abs = 0.0
max_drawdown = 0.0
drawdown_start: Optional[datetime] = None
drawdown_end: Optional[datetime] = None
dd_high_val = dd_low_val = 0.0
if len(trades_df) > 0:
try:
(max_drawdown_abs, _, _, _, _, max_drawdown) = calculate_max_drawdown(
trades_df, value_col='profit_abs', starting_balance=starting_balance)
(max_drawdown_abs, drawdown_start, drawdown_end, dd_high_val, dd_low_val,
max_drawdown) = calculate_max_drawdown(
trades_df, value_col='profit_abs', date_col='close_date_dt',
starting_balance=starting_balance)
except ValueError:
# ValueError if no losing trade.
pass
@@ -577,12 +589,12 @@ class RPC:
'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades),
'closed_trade_count': closed_trade_count,
'first_trade_date': first_date.strftime(DATETIME_PRINT_FORMAT) if first_date else '',
'first_trade_date': format_date(first_date),
'first_trade_humanized': dt_humanize(first_date) if first_date else '',
'first_trade_timestamp': int(first_date.timestamp() * 1000) if first_date else 0,
'latest_trade_date': last_date.strftime(DATETIME_PRINT_FORMAT) if last_date else '',
'first_trade_timestamp': dt_ts_def(first_date, 0),
'latest_trade_date': format_date(last_date),
'latest_trade_humanized': dt_humanize(last_date) if last_date else '',
'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0,
'latest_trade_timestamp': dt_ts_def(last_date, 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, # Deprecated
@@ -595,9 +607,15 @@ class RPC:
'expectancy_ratio': expectancy_ratio,
'max_drawdown': max_drawdown,
'max_drawdown_abs': max_drawdown_abs,
'max_drawdown_start': format_date(drawdown_start),
'max_drawdown_start_timestamp': dt_ts_def(drawdown_start),
'max_drawdown_end': format_date(drawdown_end),
'max_drawdown_end_timestamp': dt_ts_def(drawdown_end),
'drawdown_high': dd_high_val,
'drawdown_low': dd_low_val,
'trading_volume': trading_volume,
'bot_start_timestamp': int(bot_start.timestamp() * 1000) if bot_start else 0,
'bot_start_date': bot_start.strftime(DATETIME_PRINT_FORMAT) if bot_start else '',
'bot_start_timestamp': dt_ts_def(bot_start, 0),
'bot_start_date': format_date(bot_start),
}
def __balance_get_est_stake(
@@ -1106,7 +1124,7 @@ class RPC:
buffer = bufferHandler.buffer[-limit:]
else:
buffer = bufferHandler.buffer
records = [[datetime.fromtimestamp(r.created).strftime(DATETIME_PRINT_FORMAT),
records = [[format_date(datetime.fromtimestamp(r.created)),
r.created * 1000, r.name, r.levelname,
r.message + ('\n' + r.exc_text if r.exc_text else '')]
for r in buffer]
@@ -1323,7 +1341,7 @@ class RPC:
return {
"last_process": str(last_p),
"last_process_loc": last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT),
"last_process_loc": format_date(last_p.astimezone(tzlocal())),
"last_process_ts": int(last_p.timestamp()),
}
+10 -5
View File
@@ -51,6 +51,7 @@ class TimeunitMappings:
message2: str
callback: str
default: int
dateformat: str
def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
@@ -736,10 +737,10 @@ class Telegram(RPCHandler):
"""
vals = {
'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7),
'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7, '%Y-%m-%d'),
'weeks': TimeunitMappings('Monday', 'Weekly', 'weeks (starting from Monday)',
'update_weekly', 8),
'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6),
'update_weekly', 8, '%Y-%m-%d'),
'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6, '%Y-%m'),
}
val = vals[unit]
@@ -756,7 +757,7 @@ class Telegram(RPCHandler):
unit
)
stats_tab = tabulate(
[[f"{period['date']} ({period['trade_count']})",
[[f"{period['date']:{val.dateformat}} ({period['trade_count']})",
f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}",
f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}",
f"{period['rel_profit']:.2%}",
@@ -888,7 +889,11 @@ class Telegram(RPCHandler):
f"*Trading volume:* `{round_coin_value(stats['trading_volume'], stake_cur)}`\n"
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`"
f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`\n"
f" from `{stats['max_drawdown_start']} "
f"({round_coin_value(stats['drawdown_high'], stake_cur)})`\n"
f" to `{stats['max_drawdown_end']} "
f"({round_coin_value(stats['drawdown_low'], stake_cur)})`\n"
)
await self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
query=update.callback_query)
+22 -13
View File
@@ -373,7 +373,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> float:
current_profit: float, after_fill: bool, **kwargs) -> Optional[float]:
"""
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate.
@@ -389,6 +389,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param after_fill: True if the stoploss is called after the order was filled.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the current_rate
"""
@@ -719,6 +720,8 @@ class IStrategy(ABC, HyperStrategyMixin):
# END - Intended to be overridden by strategy
###
_ft_stop_uses_after_fill = False
def __informative_pairs_freqai(self) -> ListPairsWithTimeframes:
"""
Create informative-pairs needed for FreqAI
@@ -1160,13 +1163,17 @@ class IStrategy(ABC, HyperStrategyMixin):
def ft_stoploss_adjust(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float,
force_stoploss: float, low: Optional[float] = None,
high: Optional[float] = None) -> None:
high: Optional[float] = None, after_fill: bool = False) -> None:
"""
Adjust stop-loss dynamically if configured to do so.
:param current_profit: current profit as ratio
:param low: Low value of this candle, only set in backtesting
:param high: High value of this candle, only set in backtesting
"""
if after_fill and not self._ft_stop_uses_after_fill:
# Skip if the strategy doesn't support after fill.
return
stop_loss_value = force_stoploss if force_stoploss else self.stoploss
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
@@ -1181,18 +1188,20 @@ class IStrategy(ABC, HyperStrategyMixin):
bound = (low if trade.is_short else high)
bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
if self.use_custom_stoploss and dir_correct:
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None,
supress_error=True
)(pair=trade.pair, trade=trade,
current_time=current_time,
current_rate=(bound or current_rate),
current_profit=bound_profit)
stop_loss_value_custom = strategy_safe_wrapper(
self.custom_stoploss, default_retval=None, supress_error=True
)(pair=trade.pair, trade=trade,
current_time=current_time,
current_rate=(bound or current_rate),
current_profit=bound_profit,
after_fill=after_fill)
# Sanity check - error cases will return None
if stop_loss_value:
# logger.info(f"{trade.pair} {stop_loss_value=} {bound_profit=}")
trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
if stop_loss_value_custom:
stop_loss_value = stop_loss_value_custom
trade.adjust_stop_loss(bound or current_rate, stop_loss_value,
allow_refresh=after_fill)
else:
logger.warning("CustomStoploss function did not return valid stoploss")
logger.debug("CustomStoploss function did not return valid stoploss")
if self.trailing_stop and dir_correct:
# trailing stoploss handling
@@ -1245,7 +1254,7 @@ class IStrategy(ABC, HyperStrategyMixin):
exit_type = ExitType.STOP_LOSS
# If initial stoploss is not the same as current one then it is trailing.
if trade.initial_stop_loss != trade.stop_loss:
if trade.is_stop_loss_trailing:
exit_type = ExitType.TRAILING_STOP_LOSS
logger.debug(
f"{trade.pair} - HIT STOP: current price at "
+4 -2
View File
@@ -123,7 +123,8 @@ def stoploss_from_open(
return max(stoploss * leverage, 0.0)
def stoploss_from_absolute(stop_rate: float, current_rate: float, is_short: bool = False) -> float:
def stoploss_from_absolute(stop_rate: float, current_rate: float, is_short: bool = False,
leverage: float = 1.0) -> float:
"""
Given current price and desired stop price, return a stop loss value that is relative to current
price.
@@ -136,6 +137,7 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float, is_short: bool
:param stop_rate: Stop loss price.
:param current_rate: Current asset price.
:param is_short: When true, perform the calculation for short instead of long
:param leverage: Leverage to use for the calculation
:return: Positive stop loss value relative to current price
"""
@@ -150,4 +152,4 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float, is_short: bool
# negative stoploss values indicate the requested stop price is higher/lower
# (long/short) than the current price
# shorts can yield stoploss values higher than 1, so limit that as well
return max(min(stoploss, 1.0), 0.0)
return max(min(stoploss, 1.0), 0.0) * leverage
@@ -102,8 +102,8 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
current_rate: float, current_profit: float, **kwargs) -> float:
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, after_fill: bool, **kwargs) -> float:
"""
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate.
@@ -111,7 +111,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns the initial stoploss value
When not implemented by a strategy, returns the initial stoploss value.
Only called when use_custom_stoploss is set to True.
:param pair: Pair that's currently analyzed
@@ -119,10 +119,10 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param after_fill: True if the stoploss is called after the order was filled.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the current_rate
"""
return self.stoploss
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]':
+4 -1
View File
@@ -1,5 +1,6 @@
from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts,
dt_utc, format_ms_time, shorten_date)
dt_ts_def, dt_utc, format_date, format_ms_time,
shorten_date)
from freqtrade.util.ft_precise import FtPrecise
from freqtrade.util.periodic_cache import PeriodicCache
from freqtrade.util.template_renderer import render_template, render_template_with_fallback # noqa
@@ -11,7 +12,9 @@ __all__ = [
'dt_humanize',
'dt_now',
'dt_ts',
'dt_ts_def',
'dt_utc',
'format_date',
'format_ms_time',
'FtPrecise',
'PeriodicCache',
+23
View File
@@ -4,6 +4,8 @@ from typing import Optional
import arrow
from freqtrade.constants import DATETIME_PRINT_FORMAT
def dt_now() -> datetime:
"""Return the current datetime in UTC."""
@@ -26,6 +28,16 @@ def dt_ts(dt: Optional[datetime] = None) -> int:
return int(dt_now().timestamp() * 1000)
def dt_ts_def(dt: Optional[datetime], default: int = 0) -> int:
"""
Return dt in ms as a timestamp in UTC.
If dt is None, return the current datetime in UTC.
"""
if dt:
return int(dt.timestamp() * 1000)
return default
def dt_floor_day(dt: datetime) -> datetime:
"""Return the floor of the day for the given datetime."""
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
@@ -63,6 +75,17 @@ def dt_humanize(dt: datetime, **kwargs) -> str:
return arrow.get(dt).humanize(**kwargs)
def format_date(date: Optional[datetime]) -> str:
"""
Return a formatted date string.
Returns an empty string if date is None.
:param date: datetime to format
"""
if date:
return date.strftime(DATETIME_PRINT_FORMAT)
return ''
def format_ms_time(date: int) -> str:
"""
convert MS date to readable format.
+4 -4
View File
@@ -7,10 +7,10 @@
-r docs/requirements-docs.txt
coveralls==3.3.1
ruff==0.0.285
ruff==0.0.287
mypy==1.5.1
pre-commit==3.3.3
pytest==7.4.0
pre-commit==3.4.0
pytest==7.4.1
pytest-asyncio==0.21.1
pytest-cov==4.1.0
pytest-mock==3.11.1
@@ -20,7 +20,7 @@ isort==5.12.0
time-machine==2.12.0
# Convert jupyter notebooks to markdown documents
nbconvert==7.7.4
nbconvert==7.8.0
# mypy types
types-cachetools==5.3.0.6
+1 -1
View File
@@ -5,7 +5,7 @@
# Required for freqai
scikit-learn==1.1.3
joblib==1.3.2
catboost==1.2; 'arm' not in platform_machine
catboost==1.2.1; 'arm' not in platform_machine
lightgbm==4.0.0
xgboost==1.7.6
tensorboard==2.14.0
+2 -3
View File
@@ -2,8 +2,7 @@
-r requirements.txt
# Required for hyperopt
scipy==1.11.2; python_version >= '3.9'
scipy==1.10.1; python_version < '3.9'
scipy==1.11.2
scikit-learn==1.1.3
scikit-optimize==0.9.0
filelock==3.12.2
filelock==3.12.3
+7 -8
View File
@@ -1,14 +1,13 @@
numpy==1.25.2; python_version > '3.8'
numpy==1.24.3; python_version <= '3.8'
pandas==2.0.3
numpy==1.25.2
pandas==2.1.0
pandas-ta==0.3.14b
ccxt==4.0.71
ccxt==4.0.81
cryptography==41.0.3; platform_machine != 'armv7l'
cryptography==40.0.1; platform_machine == 'armv7l'
aiohttp==3.8.5
SQLAlchemy==2.0.20
python-telegram-bot==20.4
python-telegram-bot==20.5
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
arrow==1.2.3
@@ -25,7 +24,7 @@ tables==3.8.0
blosc==1.11.1
joblib==1.3.2
rich==13.5.2
pyarrow==12.0.1; platform_machine != 'armv7l'
pyarrow==13.0.0; platform_machine != 'armv7l'
# find first, C search in arrays
py_find_1st==1.1.5
@@ -39,8 +38,8 @@ orjson==3.9.5
sdnotify==0.3.2
# API Server
fastapi==0.101.1
pydantic==2.2.1
fastapi==0.103.1
pydantic==2.3.0
uvicorn==0.23.2
pyjwt==2.8.0
aiofiles==23.2.1
+14
View File
@@ -134,6 +134,20 @@ class FtRestClient:
"""
return self._get("daily", params={"timescale": days} if days else None)
def weekly(self, weeks=None):
"""Return the profits for each week, and amount of trades.
:return: json object
"""
return self._get("weekly", params={"timescale": weeks} if weeks else None)
def monthly(self, months=None):
"""Return the profits for each month, and amount of trades.
:return: json object
"""
return self._get("monthly", params={"timescale": months} if months else None)
def edge(self):
"""Return information about edge.
+3 -2
View File
@@ -14,7 +14,6 @@ classifiers =
Environment :: Console
Intended Audience :: Science/Research
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
@@ -33,7 +32,7 @@ tests_require =
pytest-mock
packages = find:
python_requires = >=3.8
python_requires = >=3.9
[options.entry_points]
console_scripts =
@@ -50,3 +49,5 @@ exclude =
__pycache__,
.eggs,
user_data,
.venv
.env
+5 -5
View File
@@ -25,7 +25,7 @@ function check_installed_python() {
exit 2
fi
for v in 11 10 9 8
for v in 11 10 9
do
PYTHON="python3.${v}"
which $PYTHON
@@ -36,7 +36,7 @@ function check_installed_python() {
fi
done
echo "No usable python found. Please make sure to have python3.8 or newer installed."
echo "No usable python found. Please make sure to have python3.9 or newer installed."
exit 1
}
@@ -192,7 +192,7 @@ function update() {
fi
updateenv
echo "Update completed."
echo_block "Don't forget to activate your virtual enviorment with 'source .venv/bin/activate'!"
echo_block "Don't forget to activate your virtual environment with 'source .venv/bin/activate'!"
}
@@ -277,7 +277,7 @@ function install() {
install_redhat
else
echo "This script does not support your OS."
echo "If you have Python version 3.8 - 3.11, pip, virtualenv, ta-lib you can continue."
echo "If you have Python version 3.9 - 3.11, pip, virtualenv, ta-lib you can continue."
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
sleep 10
fi
@@ -304,7 +304,7 @@ function help() {
echo " -p,--plot Install dependencies for Plotting scripts."
}
# Verify if 3.8+ is installed
# Verify if 3.9+ is installed
check_installed_python
case $* in
+82 -2
View File
@@ -1,9 +1,9 @@
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock
from freqtrade.enums.marginmode import MarginMode
from freqtrade.enums.tradingmode import TradingMode
from tests.conftest import get_mock_coro, get_patched_exchange
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@@ -68,3 +68,83 @@ def test_bybit_get_funding_fees(default_conf, mocker):
exchange.get_funding_fees('BTC/USDT:USDT', 1, False, now)
assert exchange._fetch_and_calculate_funding_fees.call_count == 1
def test_bybit_fetch_orders(default_conf, mocker, limit_order):
api_mock = MagicMock()
api_mock.fetch_orders = MagicMock(return_value=[
limit_order['buy'],
limit_order['sell'],
])
api_mock.fetch_open_orders = MagicMock(return_value=[limit_order['buy']])
api_mock.fetch_closed_orders = MagicMock(return_value=[limit_order['buy']])
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
start_time = datetime.now(timezone.utc) - timedelta(days=20)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='bybit')
# Not available in dry-run
assert exchange.fetch_orders('mocked', start_time) == []
assert api_mock.fetch_orders.call_count == 0
default_conf['dry_run'] = False
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='bybit')
res = exchange.fetch_orders('mocked', start_time)
# Bybit will call the endpoint 3 times, as it has a limit of 7 days per call
assert api_mock.fetch_orders.call_count == 3
assert api_mock.fetch_open_orders.call_count == 0
assert api_mock.fetch_closed_orders.call_count == 0
assert len(res) == 2 * 3
def test_bybit_fetch_order_canceled_empty(default_conf_usdt, mocker):
default_conf_usdt['dry_run'] = False
api_mock = MagicMock()
api_mock.fetch_order = MagicMock(return_value={
'id': '123',
'symbol': 'BTC/USDT',
'status': 'canceled',
'filled': 0.0,
'remaining': 0.0,
'amount': 20.0,
})
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, id='bybit')
res = exchange.fetch_order('123', 'BTC/USDT')
assert res['remaining'] is None
assert res['filled'] == 0.0
assert res['amount'] == 20.0
assert res['status'] == 'canceled'
api_mock.fetch_order = MagicMock(return_value={
'id': '123',
'symbol': 'BTC/USDT',
'status': 'canceled',
'filled': 0.0,
'remaining': 20.0,
'amount': 20.0,
})
# Don't touch orders which return correctly.
res1 = exchange.fetch_order('123', 'BTC/USDT')
assert res1['remaining'] == 20.0
assert res1['filled'] == 0.0
assert res1['amount'] == 20.0
assert res1['status'] == 'canceled'
# Reverse test - remaining is not touched
api_mock.fetch_order = MagicMock(return_value={
'id': '124',
'symbol': 'BTC/USDT',
'status': 'open',
'filled': 0.0,
'remaining': 20.0,
'amount': 20.0,
})
res2 = exchange.fetch_order('123', 'BTC/USDT')
assert res2['remaining'] == 20.0
assert res2['filled'] == 0.0
assert res2['amount'] == 20.0
assert res2['status'] == 'open'
+2 -191
View File
@@ -7,20 +7,16 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
import ccxt
import pytest
from ccxt import DECIMAL_PLACES, ROUND, ROUND_UP, TICK_SIZE, TRUNCATE
from pandas import DataFrame
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError,
InsufficientFundsError, InvalidOrderException,
OperationalException, PricingError, TemporaryError)
from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_precision,
date_minus_candles, market_is_active, price_to_precision,
timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, market_is_active,
timeframe_to_prev_date)
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
calculate_backoff, remove_exchange_credentials)
from freqtrade.exchange.exchange import amount_to_contract_precision
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from freqtrade.util import dt_now, dt_ts
from tests.conftest import (EXMS, generate_test_data_raw, get_mock_coro, get_patched_exchange,
@@ -287,87 +283,6 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
ex.validate_order_time_in_force(tif2)
@pytest.mark.parametrize("amount,precision_mode,precision,expected", [
(2.34559, 2, 4, 2.3455),
(2.34559, 2, 5, 2.34559),
(2.34559, 2, 3, 2.345),
(2.9999, 2, 3, 2.999),
(2.9909, 2, 3, 2.990),
(2.9909, 2, 0, 2),
(29991.5555, 2, 0, 29991),
(29991.5555, 2, -1, 29990),
(29991.5555, 2, -2, 29900),
# Tests for Tick-size
(2.34559, 4, 0.0001, 2.3455),
(2.34559, 4, 0.00001, 2.34559),
(2.34559, 4, 0.001, 2.345),
(2.9999, 4, 0.001, 2.999),
(2.9909, 4, 0.001, 2.990),
(2.9909, 4, 0.005, 2.99),
(2.9999, 4, 0.005, 2.995),
])
def test_amount_to_precision(amount, precision_mode, precision, expected,):
"""
Test rounds down
"""
# digits counting mode
# DECIMAL_PLACES = 2
# SIGNIFICANT_DIGITS = 3
# TICK_SIZE = 4
assert amount_to_precision(amount, precision, precision_mode) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected,rounding_mode", [
# Tests for DECIMAL_PLACES, ROUND_UP
(2.34559, 2, 4, 2.3456, ROUND_UP),
(2.34559, 2, 5, 2.34559, ROUND_UP),
(2.34559, 2, 3, 2.346, ROUND_UP),
(2.9999, 2, 3, 3.000, ROUND_UP),
(2.9909, 2, 3, 2.991, ROUND_UP),
# Tests for DECIMAL_PLACES, ROUND
(2.345600000000001, DECIMAL_PLACES, 4, 2.3456, ROUND),
(2.345551, DECIMAL_PLACES, 4, 2.3456, ROUND),
(2.49, DECIMAL_PLACES, 0, 2., ROUND),
(2.51, DECIMAL_PLACES, 0, 3., ROUND),
(5.1, DECIMAL_PLACES, -1, 10., ROUND),
(4.9, DECIMAL_PLACES, -1, 0., ROUND),
# Tests for TICK_SIZE, ROUND_UP
(2.34559, TICK_SIZE, 0.0001, 2.3456, ROUND_UP),
(2.34559, TICK_SIZE, 0.00001, 2.34559, ROUND_UP),
(2.34559, TICK_SIZE, 0.001, 2.346, ROUND_UP),
(2.9999, TICK_SIZE, 0.001, 3.000, ROUND_UP),
(2.9909, TICK_SIZE, 0.001, 2.991, ROUND_UP),
(2.9909, TICK_SIZE, 0.005, 2.995, ROUND_UP),
(2.9973, TICK_SIZE, 0.005, 3.0, ROUND_UP),
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND_UP),
(234.43, TICK_SIZE, 0.5, 234.5, ROUND_UP),
(234.53, TICK_SIZE, 0.5, 235.0, ROUND_UP),
(0.891534, TICK_SIZE, 0.0001, 0.8916, ROUND_UP),
(64968.89, TICK_SIZE, 0.01, 64968.89, ROUND_UP),
(0.000000003483, TICK_SIZE, 1e-12, 0.000000003483, ROUND_UP),
# Tests for TICK_SIZE, ROUND
(2.49, TICK_SIZE, 1., 2., ROUND),
(2.51, TICK_SIZE, 1., 3., ROUND),
(2.000000051, TICK_SIZE, 0.0000001, 2.0000001, ROUND),
(2.000000049, TICK_SIZE, 0.0000001, 2., ROUND),
(2.9909, TICK_SIZE, 0.005, 2.990, ROUND),
(2.9973, TICK_SIZE, 0.005, 2.995, ROUND),
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND),
(234.24, TICK_SIZE, 0.5, 234., ROUND),
(234.26, TICK_SIZE, 0.5, 234.5, ROUND),
# Tests for TRUNCATTE
(2.34559, 2, 4, 2.3455, TRUNCATE),
(2.34559, 2, 5, 2.34559, TRUNCATE),
(2.34559, 2, 3, 2.345, TRUNCATE),
(2.9999, 2, 3, 2.999, TRUNCATE),
(2.9909, 2, 3, 2.990, TRUNCATE),
])
def test_price_to_precision(price, precision_mode, precision, expected, rounding_mode):
assert price_to_precision(
price, precision, precision_mode, rounding_mode=rounding_mode) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
(2.34559, 2, 4, 0.0001),
(2.34559, 2, 5, 0.00001),
@@ -3640,96 +3555,6 @@ def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == expected
def test_timeframe_to_minutes():
assert timeframe_to_minutes("5m") == 5
assert timeframe_to_minutes("10m") == 10
assert timeframe_to_minutes("1h") == 60
assert timeframe_to_minutes("1d") == 1440
def test_timeframe_to_seconds():
assert timeframe_to_seconds("5m") == 300
assert timeframe_to_seconds("10m") == 600
assert timeframe_to_seconds("1h") == 3600
assert timeframe_to_seconds("1d") == 86400
def test_timeframe_to_msecs():
assert timeframe_to_msecs("5m") == 300000
assert timeframe_to_msecs("10m") == 600000
assert timeframe_to_msecs("1h") == 3600000
assert timeframe_to_msecs("1d") == 86400000
def test_timeframe_to_prev_date():
# 2019-08-12 13:22:08
date = datetime.fromtimestamp(1565616128, tz=timezone.utc)
tf_list = [
# 5m -> 2019-08-12 13:20:00
("5m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)),
# 10m -> 2019-08-12 13:20:00
("10m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)),
# 1h -> 2019-08-12 13:00:00
("1h", datetime(2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc)),
# 2h -> 2019-08-12 12:00:00
("2h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)),
# 4h -> 2019-08-12 12:00:00
("4h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)),
# 1d -> 2019-08-12 00:00:00
("1d", datetime(2019, 8, 12, 00, 00, 0, tzinfo=timezone.utc)),
]
for interval, result in tf_list:
assert timeframe_to_prev_date(interval, date) == result
date = datetime.now(tz=timezone.utc)
assert timeframe_to_prev_date("5m") < date
# Does not round
time = datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)
assert timeframe_to_prev_date('5m', time) == time
time = datetime(2019, 8, 12, 13, 0, 0, tzinfo=timezone.utc)
assert timeframe_to_prev_date('1h', time) == time
def test_timeframe_to_next_date():
# 2019-08-12 13:22:08
date = datetime.fromtimestamp(1565616128, tz=timezone.utc)
tf_list = [
# 5m -> 2019-08-12 13:25:00
("5m", datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)),
# 10m -> 2019-08-12 13:30:00
("10m", datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)),
# 1h -> 2019-08-12 14:00:00
("1h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)),
# 2h -> 2019-08-12 14:00:00
("2h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)),
# 4h -> 2019-08-12 14:00:00
("4h", datetime(2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc)),
# 1d -> 2019-08-13 00:00:00
("1d", datetime(2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc)),
]
for interval, result in tf_list:
assert timeframe_to_next_date(interval, date) == result
date = datetime.now(tz=timezone.utc)
assert timeframe_to_next_date("5m") > date
date = datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)
assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5)
def test_date_minus_candles():
date = datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)
assert date_minus_candles("5m", 3, date) == date - timedelta(minutes=15)
assert date_minus_candles("5m", 5, date) == date - timedelta(minutes=25)
assert date_minus_candles("1m", 6, date) == date - timedelta(minutes=6)
assert date_minus_candles("1h", 3, date) == date - timedelta(hours=3, minutes=25)
assert date_minus_candles("1h", 3) == timeframe_to_prev_date('1h') - timedelta(hours=3)
@pytest.mark.parametrize(
"market_symbol,base,quote,exchange,spot,margin,futures,trademode,add_dict,expected_result",
[
@@ -4623,20 +4448,6 @@ def test_amount_to_contract_precision(
assert result_size == expected_fut
@pytest.mark.parametrize('amount,precision,precision_mode,contract_size,expected', [
(1.17, 1.0, 4, 0.01, 1.17), # Tick size
(1.17, 1.0, 2, 0.01, 1.17), #
(1.16, 1.0, 4, 0.01, 1.16), #
(1.16, 1.0, 2, 0.01, 1.16), #
(1.13, 1.0, 2, 0.01, 1.13), #
(10.988, 1.0, 2, 10, 10),
(10.988, 1.0, 4, 10, 10),
])
def test_amount_to_contract_precision2(amount, precision, precision_mode, contract_size, expected):
res = amount_to_contract_precision(amount, precision, precision_mode, contract_size)
assert pytest.approx(res) == expected
@pytest.mark.parametrize('exchange_name,open_rate,is_short,trading_mode,margin_mode', [
# Bittrex
('bittrex', 2.0, False, 'spot', None),
+243
View File
@@ -1,9 +1,16 @@
# pragma pylint: disable=missing-docstring, protected-access, invalid-name
from datetime import datetime, timedelta, timezone
import pytest
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
TRUNCATE)
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import (amount_to_contract_precision, amount_to_precision,
date_minus_candles, price_to_precision, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date,
timeframe_to_seconds)
from freqtrade.exchange.check_exchange import check_exchange
from tests.conftest import log_has_re
@@ -83,3 +90,239 @@ def test_check_exchange(default_conf, caplog) -> None:
with pytest.raises(OperationalException,
match=r'This command requires a configured exchange.*'):
check_exchange(default_conf)
def test_date_minus_candles():
date = datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)
assert date_minus_candles("5m", 3, date) == date - timedelta(minutes=15)
assert date_minus_candles("5m", 5, date) == date - timedelta(minutes=25)
assert date_minus_candles("1m", 6, date) == date - timedelta(minutes=6)
assert date_minus_candles("1h", 3, date) == date - timedelta(hours=3, minutes=25)
assert date_minus_candles("1h", 3) == timeframe_to_prev_date('1h') - timedelta(hours=3)
def test_timeframe_to_minutes():
assert timeframe_to_minutes("5m") == 5
assert timeframe_to_minutes("10m") == 10
assert timeframe_to_minutes("1h") == 60
assert timeframe_to_minutes("1d") == 1440
def test_timeframe_to_seconds():
assert timeframe_to_seconds("5m") == 300
assert timeframe_to_seconds("10m") == 600
assert timeframe_to_seconds("1h") == 3600
assert timeframe_to_seconds("1d") == 86400
def test_timeframe_to_msecs():
assert timeframe_to_msecs("5m") == 300000
assert timeframe_to_msecs("10m") == 600000
assert timeframe_to_msecs("1h") == 3600000
assert timeframe_to_msecs("1d") == 86400000
def test_timeframe_to_prev_date():
# 2019-08-12 13:22:08
date = datetime.fromtimestamp(1565616128, tz=timezone.utc)
tf_list = [
# 5m -> 2019-08-12 13:20:00
("5m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)),
# 10m -> 2019-08-12 13:20:00
("10m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)),
# 1h -> 2019-08-12 13:00:00
("1h", datetime(2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc)),
# 2h -> 2019-08-12 12:00:00
("2h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)),
# 4h -> 2019-08-12 12:00:00
("4h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)),
# 1d -> 2019-08-12 00:00:00
("1d", datetime(2019, 8, 12, 00, 00, 0, tzinfo=timezone.utc)),
]
for interval, result in tf_list:
assert timeframe_to_prev_date(interval, date) == result
date = datetime.now(tz=timezone.utc)
assert timeframe_to_prev_date("5m") < date
# Does not round
time = datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)
assert timeframe_to_prev_date('5m', time) == time
time = datetime(2019, 8, 12, 13, 0, 0, tzinfo=timezone.utc)
assert timeframe_to_prev_date('1h', time) == time
def test_timeframe_to_next_date():
# 2019-08-12 13:22:08
date = datetime.fromtimestamp(1565616128, tz=timezone.utc)
tf_list = [
# 5m -> 2019-08-12 13:25:00
("5m", datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)),
# 10m -> 2019-08-12 13:30:00
("10m", datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)),
# 1h -> 2019-08-12 14:00:00
("1h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)),
# 2h -> 2019-08-12 14:00:00
("2h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)),
# 4h -> 2019-08-12 14:00:00
("4h", datetime(2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc)),
# 1d -> 2019-08-13 00:00:00
("1d", datetime(2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc)),
]
for interval, result in tf_list:
assert timeframe_to_next_date(interval, date) == result
date = datetime.now(tz=timezone.utc)
assert timeframe_to_next_date("5m") > date
date = datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)
assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5)
@pytest.mark.parametrize("amount,precision_mode,precision,expected", [
(2.34559, DECIMAL_PLACES, 4, 2.3455),
(2.34559, DECIMAL_PLACES, 5, 2.34559),
(2.34559, DECIMAL_PLACES, 3, 2.345),
(2.9999, DECIMAL_PLACES, 3, 2.999),
(2.9909, DECIMAL_PLACES, 3, 2.990),
(2.9909, DECIMAL_PLACES, 0, 2),
(29991.5555, DECIMAL_PLACES, 0, 29991),
(29991.5555, DECIMAL_PLACES, -1, 29990),
(29991.5555, DECIMAL_PLACES, -2, 29900),
# Tests for
(2.34559, SIGNIFICANT_DIGITS, 4, 2.345),
(2.34559, SIGNIFICANT_DIGITS, 5, 2.3455),
(2.34559, SIGNIFICANT_DIGITS, 3, 2.34),
(2.9999, SIGNIFICANT_DIGITS, 3, 2.99),
(2.9909, SIGNIFICANT_DIGITS, 3, 2.99),
(0.0000077723, SIGNIFICANT_DIGITS, 5, 0.0000077723),
(0.0000077723, SIGNIFICANT_DIGITS, 3, 0.00000777),
(0.0000077723, SIGNIFICANT_DIGITS, 1, 0.000007),
# Tests for Tick-size
(2.34559, TICK_SIZE, 0.0001, 2.3455),
(2.34559, TICK_SIZE, 0.00001, 2.34559),
(2.34559, TICK_SIZE, 0.001, 2.345),
(2.9999, TICK_SIZE, 0.001, 2.999),
(2.9909, TICK_SIZE, 0.001, 2.990),
(2.9909, TICK_SIZE, 0.005, 2.99),
(2.9999, TICK_SIZE, 0.005, 2.995),
])
def test_amount_to_precision(amount, precision_mode, precision, expected,):
"""
Test rounds down
"""
# digits counting mode
# DECIMAL_PLACES = 2
# SIGNIFICANT_DIGITS = 3
# TICK_SIZE = 4
assert amount_to_precision(amount, precision, precision_mode) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected,rounding_mode", [
# Tests for DECIMAL_PLACES, ROUND_UP
(2.34559, DECIMAL_PLACES, 4, 2.3456, ROUND_UP),
(2.34559, DECIMAL_PLACES, 5, 2.34559, ROUND_UP),
(2.34559, DECIMAL_PLACES, 3, 2.346, ROUND_UP),
(2.9999, DECIMAL_PLACES, 3, 3.000, ROUND_UP),
(2.9909, DECIMAL_PLACES, 3, 2.991, ROUND_UP),
(2.9901, DECIMAL_PLACES, 3, 2.991, ROUND_UP),
(2.34559, DECIMAL_PLACES, 5, 2.34559, ROUND_DOWN),
(2.34559, DECIMAL_PLACES, 4, 2.3455, ROUND_DOWN),
(2.9901, DECIMAL_PLACES, 3, 2.990, ROUND_DOWN),
(0.00299, DECIMAL_PLACES, 3, 0.002, ROUND_DOWN),
# Tests for DECIMAL_PLACES, ROUND
(2.345600000000001, DECIMAL_PLACES, 4, 2.3456, ROUND),
(2.345551, DECIMAL_PLACES, 4, 2.3456, ROUND),
(2.49, DECIMAL_PLACES, 0, 2., ROUND),
(2.51, DECIMAL_PLACES, 0, 3., ROUND),
(5.1, DECIMAL_PLACES, -1, 10., ROUND),
(4.9, DECIMAL_PLACES, -1, 0., ROUND),
(0.000007222, SIGNIFICANT_DIGITS, 1, 0.000007, ROUND),
(0.000007222, SIGNIFICANT_DIGITS, 2, 0.0000072, ROUND),
(0.000007777, SIGNIFICANT_DIGITS, 2, 0.0000078, ROUND),
# Tests for TICK_SIZE, ROUND_UP
(2.34559, TICK_SIZE, 0.0001, 2.3456, ROUND_UP),
(2.34559, TICK_SIZE, 0.00001, 2.34559, ROUND_UP),
(2.34559, TICK_SIZE, 0.001, 2.346, ROUND_UP),
(2.9999, TICK_SIZE, 0.001, 3.000, ROUND_UP),
(2.9909, TICK_SIZE, 0.001, 2.991, ROUND_UP),
(2.9909, TICK_SIZE, 0.001, 2.990, ROUND_DOWN),
(2.9909, TICK_SIZE, 0.005, 2.995, ROUND_UP),
(2.9973, TICK_SIZE, 0.005, 3.0, ROUND_UP),
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND_UP),
(234.43, TICK_SIZE, 0.5, 234.5, ROUND_UP),
(234.43, TICK_SIZE, 0.5, 234.0, ROUND_DOWN),
(234.53, TICK_SIZE, 0.5, 235.0, ROUND_UP),
(234.53, TICK_SIZE, 0.5, 234.5, ROUND_DOWN),
(0.891534, TICK_SIZE, 0.0001, 0.8916, ROUND_UP),
(64968.89, TICK_SIZE, 0.01, 64968.89, ROUND_UP),
(0.000000003483, TICK_SIZE, 1e-12, 0.000000003483, ROUND_UP),
# Tests for TICK_SIZE, ROUND
(2.49, TICK_SIZE, 1., 2., ROUND),
(2.51, TICK_SIZE, 1., 3., ROUND),
(2.000000051, TICK_SIZE, 0.0000001, 2.0000001, ROUND),
(2.000000049, TICK_SIZE, 0.0000001, 2., ROUND),
(2.9909, TICK_SIZE, 0.005, 2.990, ROUND),
(2.9973, TICK_SIZE, 0.005, 2.995, ROUND),
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND),
(234.24, TICK_SIZE, 0.5, 234., ROUND),
(234.26, TICK_SIZE, 0.5, 234.5, ROUND),
# Tests for TRUNCATTE
(2.34559, DECIMAL_PLACES, 4, 2.3455, TRUNCATE),
(2.34559, DECIMAL_PLACES, 5, 2.34559, TRUNCATE),
(2.34559, DECIMAL_PLACES, 3, 2.345, TRUNCATE),
(2.9999, DECIMAL_PLACES, 3, 2.999, TRUNCATE),
(2.9909, DECIMAL_PLACES, 3, 2.990, TRUNCATE),
(2.9909, TICK_SIZE, 0.001, 2.990, TRUNCATE),
(2.9909, TICK_SIZE, 0.01, 2.99, TRUNCATE),
(2.9909, TICK_SIZE, 0.1, 2.9, TRUNCATE),
# Tests for Significant
(2.34559, SIGNIFICANT_DIGITS, 4, 2.345, TRUNCATE),
(2.34559, SIGNIFICANT_DIGITS, 5, 2.3455, TRUNCATE),
(2.34559, SIGNIFICANT_DIGITS, 3, 2.34, TRUNCATE),
(2.9999, SIGNIFICANT_DIGITS, 3, 2.99, TRUNCATE),
(2.9909, SIGNIFICANT_DIGITS, 2, 2.9, TRUNCATE),
(0.00000777, SIGNIFICANT_DIGITS, 2, 0.0000077, TRUNCATE),
(0.00000729, SIGNIFICANT_DIGITS, 2, 0.0000072, TRUNCATE),
# ROUND
(722.2, SIGNIFICANT_DIGITS, 1, 700.0, ROUND),
(790.2, SIGNIFICANT_DIGITS, 1, 800.0, ROUND),
(722.2, SIGNIFICANT_DIGITS, 2, 720.0, ROUND),
(722.2, SIGNIFICANT_DIGITS, 1, 800.0, ROUND_UP),
(722.2, SIGNIFICANT_DIGITS, 2, 730.0, ROUND_UP),
(777.7, SIGNIFICANT_DIGITS, 2, 780.0, ROUND_UP),
(777.7, SIGNIFICANT_DIGITS, 3, 778.0, ROUND_UP),
(722.2, SIGNIFICANT_DIGITS, 1, 700.0, ROUND_DOWN),
(722.2, SIGNIFICANT_DIGITS, 2, 720.0, ROUND_DOWN),
(777.7, SIGNIFICANT_DIGITS, 2, 770.0, ROUND_DOWN),
(777.7, SIGNIFICANT_DIGITS, 3, 777.0, ROUND_DOWN),
(0.000007222, SIGNIFICANT_DIGITS, 1, 0.000008, ROUND_UP),
(0.000007222, SIGNIFICANT_DIGITS, 2, 0.0000073, ROUND_UP),
(0.000007777, SIGNIFICANT_DIGITS, 2, 0.0000078, ROUND_UP),
(0.000007222, SIGNIFICANT_DIGITS, 1, 0.000007, ROUND_DOWN),
(0.000007222, SIGNIFICANT_DIGITS, 2, 0.0000072, ROUND_DOWN),
(0.000007777, SIGNIFICANT_DIGITS, 2, 0.0000077, ROUND_DOWN),
])
def test_price_to_precision(price, precision_mode, precision, expected, rounding_mode):
assert price_to_precision(
price, precision, precision_mode, rounding_mode=rounding_mode) == expected
@pytest.mark.parametrize('amount,precision,precision_mode,contract_size,expected', [
(1.17, 1.0, 4, 0.01, 1.17), # Tick size
(1.17, 1.0, 2, 0.01, 1.17), #
(1.16, 1.0, 4, 0.01, 1.16), #
(1.16, 1.0, 2, 0.01, 1.16), #
(1.13, 1.0, 2, 0.01, 1.13), #
(10.988, 1.0, 2, 10, 10),
(10.988, 1.0, 4, 10, 10),
])
def test_amount_to_contract_precision_standalone(amount, precision, precision_mode, contract_size,
expected):
res = amount_to_contract_precision(amount, precision, precision_mode, contract_size)
assert pytest.approx(res) == expected
+37
View File
@@ -78,18 +78,28 @@ def test_set_stop_loss_liquidation(fee):
assert trade.liquidation_price == 0.11
# Stoploss does not change from liquidation price
assert trade.stop_loss == 1.8
assert trade.stop_loss_pct == -0.2
assert trade.initial_stop_loss == 1.8
# lower stop doesn't move stoploss
trade.adjust_stop_loss(1.8, 0.2)
assert trade.liquidation_price == 0.11
assert trade.stop_loss == 1.8
assert trade.stop_loss_pct == -0.2
assert trade.initial_stop_loss == 1.8
# Lower stop with "allow_refresh" does move stoploss
trade.adjust_stop_loss(1.8, 0.22, allow_refresh=True)
assert trade.liquidation_price == 0.11
assert trade.stop_loss == 1.602
assert trade.stop_loss_pct == -0.22
assert trade.initial_stop_loss == 1.8
# higher stop does move stoploss
trade.adjust_stop_loss(2.1, 0.1)
assert trade.liquidation_price == 0.11
assert pytest.approx(trade.stop_loss) == 1.994999
assert trade.stop_loss_pct == -0.1
assert trade.initial_stop_loss == 1.8
assert trade.stoploss_or_liquidation == trade.stop_loss
@@ -131,12 +141,21 @@ def test_set_stop_loss_liquidation(fee):
assert trade.liquidation_price == 3.8
# Stoploss does not change from liquidation price
assert trade.stop_loss == 2.2
assert trade.stop_loss_pct == -0.2
assert trade.initial_stop_loss == 2.2
# Stop doesn't move stop higher
trade.adjust_stop_loss(2.0, 0.3)
assert trade.liquidation_price == 3.8
assert trade.stop_loss == 2.2
assert trade.stop_loss_pct == -0.2
assert trade.initial_stop_loss == 2.2
# Stop does move stop higher with "allow_refresh"
trade.adjust_stop_loss(2.0, 0.3, allow_refresh=True)
assert trade.liquidation_price == 3.8
assert trade.stop_loss == 2.3
assert trade.stop_loss_pct == -0.3
assert trade.initial_stop_loss == 2.2
# Stoploss does move lower
@@ -144,6 +163,7 @@ def test_set_stop_loss_liquidation(fee):
trade.adjust_stop_loss(1.8, 0.1)
assert trade.liquidation_price == 1.5
assert pytest.approx(trade.stop_loss) == 1.89
assert trade.stop_loss_pct == -0.1
assert trade.initial_stop_loss == 2.2
assert trade.stoploss_or_liquidation == 1.5
@@ -1125,13 +1145,30 @@ def test_calc_profit(
leverage=lev,
fee_open=0.0025,
fee_close=fee_close,
max_stake_amount=60.0,
trading_mode=trading_mode,
funding_fees=funding_fees
)
profit_res = trade.calculate_profit(close_rate)
assert pytest.approx(profit_res.profit_abs) == round(profit, 8)
assert pytest.approx(profit_res.profit_ratio) == round(profit_ratio, 8)
val = trade.open_trade_value * (profit_res.profit_ratio) / lev
assert pytest.approx(val) == profit_res.profit_abs
assert pytest.approx(profit_res.total_profit) == round(profit, 8)
# assert pytest.approx(profit_res.total_profit_ratio) == round(profit_ratio, 8)
assert pytest.approx(trade.calc_profit(rate=close_rate)) == round(profit, 8)
assert pytest.approx(trade.calc_profit_ratio(rate=close_rate)) == round(profit_ratio, 8)
profit_res2 = trade.calculate_profit(close_rate, trade.amount, trade.open_rate)
assert pytest.approx(profit_res2.profit_abs) == round(profit, 8)
assert pytest.approx(profit_res2.profit_ratio) == round(profit_ratio, 8)
assert pytest.approx(profit_res2.total_profit) == round(profit, 8)
# assert pytest.approx(profit_res2.total_profit_ratio) == round(profit_ratio, 8)
assert pytest.approx(trade.calc_profit(close_rate, trade.amount,
trade.open_rate)) == round(profit, 8)
assert pytest.approx(trade.calc_profit_ratio(close_rate, trade.amount,
+16 -5
View File
@@ -616,6 +616,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_timeframe": "1h", "lookback_period": 2, "refresh_period": 3600}],
"BTC", "binance", ['ETH/BTC', 'LTC/BTC', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']),
# TKN/BTC is removed because it doesn't have enough candles
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_timeframe": "1d", "lookback_period": 6, "refresh_period": 86400}],
"BTC", "binance", ['LTC/BTC', 'XRP/BTC', 'ETH/BTC', 'HOT/BTC', 'NEO/BTC']),
# ftx data is already in Quote currency, therefore won't require conversion
# ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
# "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}],
@@ -626,23 +630,25 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers,
whitelist_conf['pairlists'] = pairlists
whitelist_conf['stake_currency'] = base_currency
whitelist_conf['exchange']['name'] = exchange
# Ensure we have 6 candles
ohlcv_history_long = pd.concat([ohlcv_history, ohlcv_history])
ohlcv_history_high_vola = ohlcv_history.copy()
ohlcv_history_high_vola = ohlcv_history_long.copy()
ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index == 1, 'close'] = 0.00090
# create candles for medium overall volume with last candle high volume
ohlcv_history_medium_volume = ohlcv_history.copy()
ohlcv_history_medium_volume = ohlcv_history_long.copy()
ohlcv_history_medium_volume.loc[ohlcv_history_medium_volume.index == 2, 'volume'] = 5
# create candles for high volume with all candles high volume, but very low price.
ohlcv_history_high_volume = ohlcv_history.copy()
ohlcv_history_high_volume = ohlcv_history_long.copy()
ohlcv_history_high_volume['volume'] = 10
ohlcv_history_high_volume['low'] = ohlcv_history_high_volume.loc[:, 'low'] * 0.01
ohlcv_history_high_volume['high'] = ohlcv_history_high_volume.loc[:, 'high'] * 0.01
ohlcv_history_high_volume['close'] = ohlcv_history_high_volume.loc[:, 'close'] * 0.01
ohlcv_data = {
('ETH/BTC', '1d', CandleType.SPOT): ohlcv_history,
('ETH/BTC', '1d', CandleType.SPOT): ohlcv_history_long,
('TKN/BTC', '1d', CandleType.SPOT): ohlcv_history,
('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history_medium_volume,
('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history_high_vola,
@@ -1370,7 +1376,12 @@ def test_expand_pairlist(wildcardlist, pairs, expected):
(['BTC/USD'],
['BTC/USD', 'BTC/USDT'],
['BTC/USD']),
(['BTC/USDT:USDT'],
['BTC/USDT:USDT', 'BTC/USDT'],
['BTC/USDT:USDT']),
(['BB_BTC/USDT', 'CC_BTC/USDT', 'AA_ETH/USDT', 'XRP/USDT', 'ETH/USDT', 'XX_BTC/USDT'],
['BTC/USDT', 'ETH/USDT'],
['XRP/USDT', 'ETH/USDT']),
])
def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected):
if expected is None:
+1 -1
View File
@@ -163,7 +163,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
response = deepcopy(gen_response)
response.update({
'max_stake_amount': 0.001,
'total_profit_ratio': pytest.approx(-0.00409),
'total_profit_ratio': pytest.approx(-0.00409153),
})
assert results[0] == response
+48 -2
View File
@@ -617,6 +617,47 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
assert rc.json()['data'][0]['date'] == str(datetime.now(timezone.utc).date())
def test_api_weekly(botclient, mocker, ticker, fee, markets, time_machine):
ftbot, client = botclient
patch_get_signal(ftbot)
mocker.patch.multiple(
EXMS,
get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker,
get_fee=fee,
markets=PropertyMock(return_value=markets)
)
time_machine.move_to("2023-03-31 21:45:05 +00:00")
rc = client_get(client, f"{BASE_URI}/weekly")
assert_response(rc)
assert len(rc.json()['data']) == 4
assert rc.json()['stake_currency'] == 'BTC'
assert rc.json()['fiat_display_currency'] == 'USD'
# Moved to monday
assert rc.json()['data'][0]['date'] == '2023-03-27'
assert rc.json()['data'][1]['date'] == '2023-03-20'
def test_api_monthly(botclient, mocker, ticker, fee, markets, time_machine):
ftbot, client = botclient
patch_get_signal(ftbot)
mocker.patch.multiple(
EXMS,
get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker,
get_fee=fee,
markets=PropertyMock(return_value=markets)
)
time_machine.move_to("2023-03-31 21:45:05 +00:00")
rc = client_get(client, f"{BASE_URI}/monthly")
assert_response(rc)
assert len(rc.json()['data']) == 3
assert rc.json()['stake_currency'] == 'BTC'
assert rc.json()['fiat_display_currency'] == 'USD'
assert rc.json()['data'][0]['date'] == '2023-03-01'
assert rc.json()['data'][1]['date'] == '2023-02-01'
@pytest.mark.parametrize('is_short', [True, False])
def test_api_trades(botclient, mocker, fee, markets, is_short):
ftbot, client = botclient
@@ -936,6 +977,10 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
'expectancy_ratio': expected['expectancy_ratio'],
'max_drawdown': ANY,
'max_drawdown_abs': ANY,
'max_drawdown_start': ANY,
'max_drawdown_start_timestamp': ANY,
'max_drawdown_end': ANY,
'max_drawdown_end_timestamp': ANY,
'trading_volume': expected['trading_volume'],
'bot_start_timestamp': 0,
'bot_start_date': '',
@@ -985,7 +1030,7 @@ def test_api_performance(botclient, fee):
fee_close=fee.return_value,
fee_open=fee.return_value,
close_rate=0.265441,
leverage=1.0,
)
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
@@ -1000,7 +1045,8 @@ def test_api_performance(botclient, fee):
is_open=False,
fee_close=fee.return_value,
fee_open=fee.return_value,
close_rate=0.391
close_rate=0.391,
leverage=1.0,
)
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
+2 -1
View File
@@ -52,4 +52,5 @@ def test_strategy_test_v3(dataframe_1m, fee, is_short, side):
side=side) is True
assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(),
current_rate=20_000, current_profit=0.05) == strategy.stoploss
current_rate=20_000, current_profit=0.05, after_fill=False
) == strategy.stoploss
+2
View File
@@ -503,6 +503,7 @@ def test_custom_exit(default_conf, fee, caplog) -> None:
fee_close=fee.return_value,
exchange='binance',
open_rate=1,
leverage=1.0,
)
now = dt_now()
@@ -552,6 +553,7 @@ def test_should_sell(default_conf, fee) -> None:
fee_close=fee.return_value,
exchange='binance',
open_rate=1,
leverage=1.0,
)
now = dt_now()
res = strategy.should_exit(trade, 1, now,
+4 -1
View File
@@ -211,15 +211,18 @@ def test_stoploss_from_absolute():
assert pytest.approx(stoploss_from_absolute(110, 100)) == 0
assert pytest.approx(stoploss_from_absolute(100, 0)) == 1
assert pytest.approx(stoploss_from_absolute(0, 100)) == 1
assert pytest.approx(stoploss_from_absolute(0, 100, False, leverage=5)) == 5
assert pytest.approx(stoploss_from_absolute(90, 100, True)) == 0
assert pytest.approx(stoploss_from_absolute(100, 100, True)) == 0
assert pytest.approx(stoploss_from_absolute(110, 100, True)) == -(1 - (110 / 100))
assert pytest.approx(stoploss_from_absolute(110, 100, True)) == 0.1
assert pytest.approx(stoploss_from_absolute(105, 100, True)) == 0.05
assert pytest.approx(stoploss_from_absolute(105, 100, True, 5)) == 0.05 * 5
assert pytest.approx(stoploss_from_absolute(100, 0, True)) == 1
assert pytest.approx(stoploss_from_absolute(0, 100, True)) == 0
assert pytest.approx(stoploss_from_absolute(100, 1, True)) == 1
assert pytest.approx(stoploss_from_absolute(100, 1, is_short=True)) == 1
assert pytest.approx(stoploss_from_absolute(100, 1, is_short=True, leverage=5)) == 5
@pytest.mark.parametrize('trading_mode', ['futures', 'spot'])
+21 -13
View File
@@ -3450,7 +3450,11 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
assert cancel_order_mock.call_count == 1
def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
@pytest.mark.parametrize('is_short', [True, False])
@pytest.mark.parametrize('leverage', [1, 5])
@pytest.mark.parametrize('amount', [2, 50])
def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee, is_short,
leverage, amount) -> None:
send_msg_mock = patch_RPCManager(mocker)
patch_exchange(mocker)
cancel_order_mock = MagicMock()
@@ -3458,7 +3462,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
EXMS,
cancel_order=cancel_order_mock,
)
mocker.patch(f'{EXMS}.get_rate', return_value=0.245441)
entry_price = 0.245441
mocker.patch(f'{EXMS}.get_rate', return_value=entry_price)
mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.2)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee')
@@ -3466,28 +3472,30 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
freqtrade = FreqtradeBot(default_conf_usdt)
trade = Trade(
pair='LTC/ETH',
amount=2,
pair='LTC/USDT',
amount=amount * leverage,
exchange='binance',
open_rate=0.245441,
open_rate=entry_price,
open_date=dt_now() - timedelta(days=2),
fee_open=fee.return_value,
fee_close=fee.return_value,
close_rate=0.555,
close_date=dt_now(),
exit_reason="sell_reason_whatever",
stake_amount=0.245441 * 2,
stake_amount=entry_price * amount,
leverage=leverage,
is_short=is_short,
)
trade.orders = [
Order(
ft_order_side='buy',
ft_order_side=entry_side(is_short),
ft_pair=trade.pair,
ft_is_open=False,
order_id='buy_123456',
status="closed",
symbol=trade.pair,
order_type="market",
side="buy",
side=entry_side(is_short),
price=trade.open_rate,
average=trade.open_rate,
filled=trade.amount,
@@ -3497,14 +3505,14 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
order_filled_date=trade.open_date,
),
Order(
ft_order_side='sell',
ft_order_side=exit_side(is_short),
ft_pair=trade.pair,
ft_is_open=True,
order_id='sell_123456',
status="open",
symbol=trade.pair,
order_type="limit",
side="sell",
side=exit_side(is_short),
price=trade.open_rate,
average=trade.open_rate,
filled=0.0,
@@ -3530,8 +3538,8 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
send_msg_mock.reset_mock()
# Partial exit - below exit threshold
order['amount'] = 2
order['filled'] = 1.9
order['amount'] = amount * leverage
order['filled'] = amount * 0.99 * leverage
assert not freqtrade.handle_cancel_exit(trade, order, order['id'], reason)
# Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order_mock.call_count == 1
@@ -3550,7 +3558,7 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
send_msg_mock.reset_mock()
order['filled'] = 1
order['filled'] = amount * 0.5 * leverage
assert freqtrade.handle_cancel_exit(trade, order, order['id'], reason)
assert send_msg_mock.call_count == 1
assert (send_msg_mock.call_args_list[0][0][0]['reason']
+70
View File
@@ -495,6 +495,76 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert freqtrade.strategy.adjust_entry_price.call_count == 0
@pytest.mark.parametrize('leverage', [1, 2])
@pytest.mark.parametrize("is_short", [False, True])
def test_dca_order_adjust_entry_replace_fails(
default_conf_usdt, ticker_usdt, fee, mocker, caplog, is_short, leverage
) -> None:
spot = leverage == 1
if not spot:
default_conf_usdt['trading_mode'] = 'futures'
default_conf_usdt['margin_mode'] = 'isolated'
default_conf_usdt['position_adjustment_enable'] = True
default_conf_usdt['max_open_trades'] = 2
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch.multiple(
EXMS,
fetch_ticker=ticker_usdt,
get_fee=fee,
get_funding_fees=MagicMock(return_value=0),
)
# no order fills.
mocker.patch(f'{EXMS}._dry_is_price_crossed', side_effect=[False, True])
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
freqtrade.enter_positions()
trades = Trade.session.scalars(
select(Trade).filter(Trade.open_order_id.is_not(None))).all()
assert len(trades) == 1
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False)
# Timeout to not interfere
freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False)
# Create DCA order for 2nd trade (so we have 2 open orders on 2 trades)
# this 2nd order won't fill.
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=20)
freqtrade.process()
assert freqtrade.strategy.adjust_trade_position.call_count == 1
trades = Trade.session.scalars(
select(Trade).filter(Trade.open_order_id.is_not(None))).all()
assert len(trades) == 2
# We now have 2 orders open
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.05)
freqtrade.manage_open_orders()
trades = Trade.session.scalars(
select(Trade).filter(Trade.open_order_id.is_not(None))).all()
assert len(trades) == 2
assert len(Order.get_open_orders()) == 2
# Entry adjustment is called
assert freqtrade.strategy.adjust_entry_price.call_count == 2
# Attempt order replacement - fails.
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234)
entry_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_entry',
return_value=False)
msg = r"Could not replace order for.*"
assert not log_has_re(msg, caplog)
freqtrade.manage_open_orders()
assert log_has_re(msg, caplog)
assert entry_mock.call_count == 2
assert len(Trade.get_trades().all()) == 1
assert len(Order.get_open_orders()) == 0
@pytest.mark.parametrize('leverage', [1, 2])
def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, leverage) -> None:
default_conf_usdt['position_adjustment_enable'] = True
+20 -2
View File
@@ -3,8 +3,8 @@ from datetime import datetime, timedelta, timezone
import pytest
import time_machine
from freqtrade.util import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts, dt_utc,
format_ms_time, shorten_date)
from freqtrade.util import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts, dt_ts_def, dt_utc,
format_date, format_ms_time, shorten_date)
def test_dt_now():
@@ -22,6 +22,13 @@ def test_dt_now():
assert dt_ts(now) == int(now.timestamp() * 1000)
def test_dt_ts_def():
assert dt_ts_def(None) == 0
assert dt_ts_def(None, 123) == 123
assert dt_ts_def(datetime(2023, 5, 5, tzinfo=timezone.utc)) == 1683244800000
assert dt_ts_def(datetime(2023, 5, 5, tzinfo=timezone.utc), 123) == 1683244800000
def test_dt_utc():
assert dt_utc(2023, 5, 5) == datetime(2023, 5, 5, tzinfo=timezone.utc)
assert dt_utc(2023, 5, 5, 0, 0, 0, 555500) == datetime(2023, 5, 5, 0, 0, 0, 555500,
@@ -70,3 +77,14 @@ def test_format_ms_time() -> None:
# Date 2017-12-13 08:02:01
date_in_epoch_ms = 1513152121000
assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
def test_format_date() -> None:
date = datetime(2023, 9, 1, 5, 2, 3, 455555, tzinfo=timezone.utc)
assert format_date(date) == '2023-09-01 05:02:03'
assert format_date(None) == ''
date = datetime(2021, 9, 30, 22, 59, 3, 455555, tzinfo=timezone.utc)
assert format_date(date) == '2021-09-30 22:59:03'
assert format_date(None) == ''