Merge branch 'develop' into pr/Axel-CH/8779
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
BIN
Binary file not shown.
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||

|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
```
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2023.8.dev'
|
||||
__version__ = '2023.9-dev'
|
||||
|
||||
if 'dev' in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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()),
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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]]':
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) == ''
|
||||
|
||||
Reference in New Issue
Block a user