Merge branch 'develop' into startup-time
This commit is contained in:
@@ -25,7 +25,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ]
|
os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ]
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
|
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
|
||||||
|
|
||||||
- name: Coveralls
|
- name: Coveralls
|
||||||
if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04')
|
||||||
env:
|
env:
|
||||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||||
@@ -139,10 +139,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "macos-12", "macos-13", "macos-14" ]
|
os: [ "macos-12", "macos-13", "macos-14" ]
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
exclude:
|
|
||||||
- os: "macos-14"
|
|
||||||
python-version: "3.9"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -263,7 +260,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest ]
|
os: [ windows-latest ]
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.6.7'
|
rev: 'v0.6.8'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.12.6-slim-bookworm as base
|
FROM python:3.12.7-slim-bookworm as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] **Based on Python 3.9+**: For botting on any operating system - Windows, macOS and Linux.
|
- [x] **Based on Python 3.10+**: For botting on any operating system - Windows, macOS and Linux.
|
||||||
- [x] **Persistence**: Persistence is achieved through sqlite.
|
- [x] **Persistence**: Persistence is achieved through sqlite.
|
||||||
- [x] **Dry-run**: Run the bot without paying money.
|
- [x] **Dry-run**: Run the bot without paying money.
|
||||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||||
@@ -218,7 +218,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
|||||||
|
|
||||||
### Software requirements
|
### Software requirements
|
||||||
|
|
||||||
- [Python >= 3.9](http://docs.python-guide.org/en/latest/starting/installation/)
|
- [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+1
-1
@@ -85,7 +85,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
|
|||||||
|
|
||||||
Alternatively
|
Alternatively
|
||||||
|
|
||||||
- Python 3.9+
|
- Python 3.10+
|
||||||
- pip (pip3)
|
- pip (pip3)
|
||||||
- git
|
- git
|
||||||
- TA-Lib
|
- 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).
|
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
|
!!! Note
|
||||||
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.
|
Python3.10 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.
|
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
|
||||||
|
|
||||||
!!! Warning "Up-to-date clock"
|
!!! Warning "Up-to-date clock"
|
||||||
@@ -42,7 +42,7 @@ These requirements apply to both [Script Installation](#script-installation) and
|
|||||||
|
|
||||||
### Install guide
|
### Install guide
|
||||||
|
|
||||||
* [Python >= 3.9](http://docs.python-guide.org/en/latest/starting/installation/)
|
* [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
* [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 section below is necessary for all systems.
|
OS Specific steps are listed first, the common section below is necessary for all systems.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python3.9 or higher and the corresponding pip are assumed to be available.
|
Python3.10 or higher and the corresponding pip are assumed to be available.
|
||||||
|
|
||||||
=== "Debian/Ubuntu"
|
=== "Debian/Ubuntu"
|
||||||
#### Install necessary dependencies
|
#### Install necessary dependencies
|
||||||
@@ -69,7 +69,7 @@ OS Specific steps are listed first, the common section below is necessary for al
|
|||||||
|
|
||||||
=== "RaspberryPi/Raspbian"
|
=== "RaspberryPi/Raspbian"
|
||||||
The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/).
|
The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/).
|
||||||
This image comes with python3.9 preinstalled, making it easy to get freqtrade up and running.
|
This image comes with python3.11 preinstalled, making it easy to get freqtrade up and running.
|
||||||
|
|
||||||
Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
|
Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
|
|||||||
** --install **
|
** --install **
|
||||||
|
|
||||||
With this option, the script will install the bot and most dependencies:
|
With this option, the script will install the bot and most dependencies:
|
||||||
You will need to have git and python3.9+ installed beforehand for this to work.
|
You will need to have git and python3.10+ installed beforehand for this to work.
|
||||||
|
|
||||||
* Mandatory software as: `ta-lib`
|
* Mandatory software as: `ta-lib`
|
||||||
* Setup your virtualenv under `.venv/`
|
* Setup your virtualenv under `.venv/`
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ Once all positions are sold, run `/stop` to completely stop the bot.
|
|||||||
`/reload_config` resets "max_open_trades" to the value set in the configuration and resets this command.
|
`/reload_config` resets "max_open_trades" to the value set in the configuration and resets this command.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
||||||
|
|
||||||
### /status
|
### /status
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ We **strongly** recommend that Windows users use [Docker](docker_quickstart.md)
|
|||||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||||
Otherwise, please follow the instructions below.
|
Otherwise, please follow the instructions below.
|
||||||
|
|
||||||
All instructions assume that python 3.9+ is installed and available.
|
All instructions assume that python 3.10+ is installed and available.
|
||||||
|
|
||||||
## Clone the git repository
|
## Clone the git repository
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ cd freqtrade
|
|||||||
|
|
||||||
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
|
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#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, 3.11 and 3.12) 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.10, 3.11 and 3.12) and for 64bit Windows.
|
||||||
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
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.
|
Other versions must be downloaded from the above link.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
__main__.py for Freqtrade
|
__main__.py for Freqtrade
|
||||||
To launch Freqtrade as a module
|
To launch Freqtrade as a module
|
||||||
|
|
||||||
> python -m freqtrade (with Python >= 3.9)
|
> python -m freqtrade (with Python >= 3.10)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from freqtrade import main
|
from freqtrade import main
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
@@ -19,9 +18,6 @@ def start_strategy_update(args: Dict[str, Any]) -> None:
|
|||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
|
||||||
if sys.version_info == (3, 8): # pragma: no cover
|
|
||||||
sys.exit("Freqtrade strategy updater requires Python version >= 3.9")
|
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
strategy_objs = StrategyResolver.search_all_objects(
|
strategy_objs = StrategyResolver.search_all_objects(
|
||||||
|
|||||||
@@ -144,6 +144,29 @@ class Binance(Exchange):
|
|||||||
"""
|
"""
|
||||||
return open_date.minute == 0 and open_date.second < 15
|
return open_date.minute == 0 and open_date.second < 15
|
||||||
|
|
||||||
|
def fetch_funding_rates(
|
||||||
|
self, symbols: Optional[List[str]] = None
|
||||||
|
) -> Dict[str, Dict[str, float]]:
|
||||||
|
"""
|
||||||
|
Fetch funding rates for the given symbols.
|
||||||
|
:param symbols: List of symbols to fetch funding rates for
|
||||||
|
:return: Dict of funding rates for the given symbols
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
rates = self._api.fetch_funding_rates(symbols)
|
||||||
|
return rates
|
||||||
|
return {}
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def dry_run_liquidation_price(
|
def dry_run_liquidation_price(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
@@ -153,8 +176,7 @@ class Binance(Exchange):
|
|||||||
stake_amount: float,
|
stake_amount: float,
|
||||||
leverage: float,
|
leverage: float,
|
||||||
wallet_balance: float, # Or margin balance
|
wallet_balance: float, # Or margin balance
|
||||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
open_trades: list,
|
||||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
|
||||||
) -> Optional[float]:
|
) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Important: Must be fetching data from cached values as this is used by backtesting!
|
Important: Must be fetching data from cached values as this is used by backtesting!
|
||||||
@@ -172,6 +194,7 @@ class Binance(Exchange):
|
|||||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||||
Cross-Margin Mode: crossWalletBalance
|
Cross-Margin Mode: crossWalletBalance
|
||||||
Isolated-Margin Mode: isolatedWalletBalance
|
Isolated-Margin Mode: isolatedWalletBalance
|
||||||
|
:param open_trades: List of open trades in the same wallet
|
||||||
|
|
||||||
# * Only required for Cross
|
# * Only required for Cross
|
||||||
:param mm_ex_1: (TMM)
|
:param mm_ex_1: (TMM)
|
||||||
@@ -180,15 +203,41 @@ class Binance(Exchange):
|
|||||||
:param upnl_ex_1: (UPNL)
|
:param upnl_ex_1: (UPNL)
|
||||||
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
|
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
|
||||||
Isolated-Margin Mode: 0
|
Isolated-Margin Mode: 0
|
||||||
|
:param other
|
||||||
"""
|
"""
|
||||||
|
cross_vars: float = 0.0
|
||||||
side_1 = -1 if is_short else 1
|
|
||||||
cross_vars = upnl_ex_1 - mm_ex_1 if self.margin_mode == MarginMode.CROSS else 0.0
|
|
||||||
|
|
||||||
# mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
|
# mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
|
||||||
# maintenance_amt: (CUM) Maintenance Amount of position
|
# maintenance_amt: (CUM) Maintenance Amount of position
|
||||||
mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, stake_amount)
|
mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, stake_amount)
|
||||||
|
|
||||||
|
if self.margin_mode == MarginMode.CROSS:
|
||||||
|
mm_ex_1: float = 0.0
|
||||||
|
upnl_ex_1: float = 0.0
|
||||||
|
pairs = [trade.pair for trade in open_trades]
|
||||||
|
if self._config["runmode"] in ("live", "dry_run"):
|
||||||
|
funding_rates = self.fetch_funding_rates(pairs)
|
||||||
|
for trade in open_trades:
|
||||||
|
if trade.pair == pair:
|
||||||
|
# Only "other" trades are considered
|
||||||
|
continue
|
||||||
|
if self._config["runmode"] in ("live", "dry_run"):
|
||||||
|
mark_price = funding_rates[trade.pair]["markPrice"]
|
||||||
|
else:
|
||||||
|
# Fall back to open rate for backtesting
|
||||||
|
mark_price = trade.open_rate
|
||||||
|
mm_ratio1, maint_amnt1 = self.get_maintenance_ratio_and_amt(
|
||||||
|
trade.pair, trade.stake_amount
|
||||||
|
)
|
||||||
|
maint_margin = trade.amount * mark_price * mm_ratio1 - maint_amnt1
|
||||||
|
mm_ex_1 += maint_margin
|
||||||
|
|
||||||
|
upnl_ex_1 += trade.amount * mark_price - trade.amount * trade.open_rate
|
||||||
|
|
||||||
|
cross_vars = upnl_ex_1 - mm_ex_1
|
||||||
|
|
||||||
|
side_1 = -1 if is_short else 1
|
||||||
|
|
||||||
if maintenance_amt is None:
|
if maintenance_amt is None:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"Parameter maintenance_amt is required by Binance.liquidation_price"
|
"Parameter maintenance_amt is required by Binance.liquidation_price"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -147,8 +147,7 @@ class Bybit(Exchange):
|
|||||||
stake_amount: float,
|
stake_amount: float,
|
||||||
leverage: float,
|
leverage: float,
|
||||||
wallet_balance: float, # Or margin balance
|
wallet_balance: float, # Or margin balance
|
||||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
open_trades: list,
|
||||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
|
||||||
) -> Optional[float]:
|
) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Important: Must be fetching data from cached values as this is used by backtesting!
|
Important: Must be fetching data from cached values as this is used by backtesting!
|
||||||
@@ -178,6 +177,7 @@ class Bybit(Exchange):
|
|||||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||||
Cross-Margin Mode: crossWalletBalance
|
Cross-Margin Mode: crossWalletBalance
|
||||||
Isolated-Margin Mode: isolatedWalletBalance
|
Isolated-Margin Mode: isolatedWalletBalance
|
||||||
|
:param open_trades: List of other open trades in the same wallet
|
||||||
"""
|
"""
|
||||||
|
|
||||||
market = self.markets[pair]
|
market = self.markets[pair]
|
||||||
|
|||||||
@@ -3532,8 +3532,7 @@ class Exchange:
|
|||||||
stake_amount: float,
|
stake_amount: float,
|
||||||
leverage: float,
|
leverage: float,
|
||||||
wallet_balance: float,
|
wallet_balance: float,
|
||||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
open_trades: Optional[list] = None,
|
||||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
|
||||||
) -> Optional[float]:
|
) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Set's the margin mode on the exchange to cross or isolated for a specific pair
|
Set's the margin mode on the exchange to cross or isolated for a specific pair
|
||||||
@@ -3555,8 +3554,7 @@ class Exchange:
|
|||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
wallet_balance=wallet_balance,
|
wallet_balance=wallet_balance,
|
||||||
mm_ex_1=mm_ex_1,
|
open_trades=open_trades or [],
|
||||||
upnl_ex_1=upnl_ex_1,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
positions = self.fetch_positions(pair)
|
positions = self.fetch_positions(pair)
|
||||||
@@ -3582,8 +3580,7 @@ class Exchange:
|
|||||||
stake_amount: float,
|
stake_amount: float,
|
||||||
leverage: float,
|
leverage: float,
|
||||||
wallet_balance: float, # Or margin balance
|
wallet_balance: float, # Or margin balance
|
||||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
open_trades: list,
|
||||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
|
||||||
) -> Optional[float]:
|
) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Important: Must be fetching data from cached values as this is used by backtesting!
|
Important: Must be fetching data from cached values as this is used by backtesting!
|
||||||
@@ -3608,10 +3605,7 @@ class Exchange:
|
|||||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||||
Cross-Margin Mode: crossWalletBalance
|
Cross-Margin Mode: crossWalletBalance
|
||||||
Isolated-Margin Mode: isolatedWalletBalance
|
Isolated-Margin Mode: isolatedWalletBalance
|
||||||
|
:param open_trades: List of other open trades in the same wallet
|
||||||
# * Not required by Gate or OKX
|
|
||||||
:param mm_ex_1:
|
|
||||||
:param upnl_ex_1:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
market = self.markets[pair]
|
market = self.markets[pair]
|
||||||
|
|||||||
+19
-14
@@ -43,6 +43,7 @@ from freqtrade.exchange import (
|
|||||||
timeframe_to_next_date,
|
timeframe_to_next_date,
|
||||||
timeframe_to_seconds,
|
timeframe_to_seconds,
|
||||||
)
|
)
|
||||||
|
from freqtrade.leverage.liquidation_price import update_liquidation_prices
|
||||||
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
from freqtrade.persistence import Order, PairLocks, Trade, init_db
|
from freqtrade.persistence import Order, PairLocks, Trade, init_db
|
||||||
@@ -241,6 +242,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Only update open orders on startup
|
# Only update open orders on startup
|
||||||
# This will update the database after the initial migration
|
# This will update the database after the initial migration
|
||||||
self.startup_update_open_orders()
|
self.startup_update_open_orders()
|
||||||
|
self.update_all_liquidation_prices()
|
||||||
self.update_funding_fees()
|
self.update_funding_fees()
|
||||||
|
|
||||||
def process(self) -> None:
|
def process(self) -> None:
|
||||||
@@ -357,6 +359,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
open_trades = Trade.get_open_trade_count()
|
open_trades = Trade.get_open_trade_count()
|
||||||
return max(0, self.config["max_open_trades"] - open_trades)
|
return max(0, self.config["max_open_trades"] - open_trades)
|
||||||
|
|
||||||
|
def update_all_liquidation_prices(self) -> None:
|
||||||
|
if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.CROSS:
|
||||||
|
# Update liquidation prices for all trades in cross margin mode
|
||||||
|
update_liquidation_prices(
|
||||||
|
exchange=self.exchange,
|
||||||
|
wallets=self.wallets,
|
||||||
|
stake_currency=self.config["stake_currency"],
|
||||||
|
dry_run=self.config["dry_run"],
|
||||||
|
)
|
||||||
|
|
||||||
def update_funding_fees(self) -> None:
|
def update_funding_fees(self) -> None:
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
trades: List[Trade] = Trade.get_open_trades()
|
trades: List[Trade] = Trade.get_open_trades()
|
||||||
@@ -2233,20 +2245,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Must also run for partial exits
|
# Must also run for partial exits
|
||||||
# TODO: Margin will need to use interest_rate as well.
|
# TODO: Margin will need to use interest_rate as well.
|
||||||
# interest_rate = self.exchange.get_interest_rate()
|
# interest_rate = self.exchange.get_interest_rate()
|
||||||
try:
|
update_liquidation_prices(
|
||||||
trade.set_liquidation_price(
|
trade,
|
||||||
self.exchange.get_liquidation_price(
|
exchange=self.exchange,
|
||||||
pair=trade.pair,
|
wallets=self.wallets,
|
||||||
open_rate=trade.open_rate,
|
stake_currency=self.config["stake_currency"],
|
||||||
is_short=trade.is_short,
|
dry_run=self.config["dry_run"],
|
||||||
amount=trade.amount,
|
)
|
||||||
stake_amount=trade.stake_amount,
|
|
||||||
leverage=trade.leverage,
|
|
||||||
wallet_balance=trade.stake_amount,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except DependencyException:
|
|
||||||
logger.warning("Unable to calculate liquidation price")
|
|
||||||
if self.strategy.use_custom_stoploss:
|
if self.strategy.use_custom_stoploss:
|
||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
trade.pair, side="exit", is_short=trade.is_short, refresh=True
|
trade.pair, side="exit", is_short=trade.is_short, refresh=True
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from freqtrade.enums import MarginMode
|
||||||
|
from freqtrade.exceptions import DependencyException
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.persistence import LocalTrade, Trade
|
||||||
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def update_liquidation_prices(
|
||||||
|
trade: Optional[LocalTrade] = None,
|
||||||
|
*,
|
||||||
|
exchange: Exchange,
|
||||||
|
wallets: Wallets,
|
||||||
|
stake_currency: str,
|
||||||
|
dry_run: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update trade liquidation price in isolated margin mode.
|
||||||
|
Updates liquidation price for all trades in cross margin mode.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if exchange.margin_mode == MarginMode.CROSS:
|
||||||
|
total_wallet_stake = 0.0
|
||||||
|
if dry_run:
|
||||||
|
# Parameters only needed for cross margin
|
||||||
|
total_wallet_stake = wallets.get_total(stake_currency)
|
||||||
|
|
||||||
|
logger.info("Updating liquidation price for all open trades.")
|
||||||
|
open_trades = Trade.get_open_trades()
|
||||||
|
for t in open_trades:
|
||||||
|
# TODO: This should be done in a batch update
|
||||||
|
t.set_liquidation_price(
|
||||||
|
exchange.get_liquidation_price(
|
||||||
|
pair=t.pair,
|
||||||
|
open_rate=t.open_rate,
|
||||||
|
is_short=t.is_short,
|
||||||
|
amount=t.amount,
|
||||||
|
stake_amount=t.stake_amount,
|
||||||
|
leverage=t.leverage,
|
||||||
|
wallet_balance=total_wallet_stake,
|
||||||
|
open_trades=open_trades,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif trade:
|
||||||
|
trade.set_liquidation_price(
|
||||||
|
exchange.get_liquidation_price(
|
||||||
|
pair=trade.pair,
|
||||||
|
open_rate=trade.open_rate,
|
||||||
|
is_short=trade.is_short,
|
||||||
|
amount=trade.amount,
|
||||||
|
stake_amount=trade.stake_amount,
|
||||||
|
leverage=trade.leverage,
|
||||||
|
wallet_balance=trade.stake_amount,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise DependencyException(
|
||||||
|
"Trade object is required for updating liquidation price in isolated margin mode."
|
||||||
|
)
|
||||||
|
except DependencyException:
|
||||||
|
logger.warning("Unable to calculate liquidation price")
|
||||||
+2
-2
@@ -10,8 +10,8 @@ from typing import Any, List, Optional
|
|||||||
|
|
||||||
|
|
||||||
# check min. python version
|
# check min. python version
|
||||||
if sys.version_info < (3, 9): # pragma: no cover
|
if sys.version_info < (3, 10): # pragma: no cover
|
||||||
sys.exit("Freqtrade requires Python version >= 3.9")
|
sys.exit("Freqtrade requires Python version >= 3.10")
|
||||||
|
|
||||||
from freqtrade import __version__
|
from freqtrade import __version__
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from freqtrade.enums import (
|
|||||||
CandleType,
|
CandleType,
|
||||||
ExitCheckTuple,
|
ExitCheckTuple,
|
||||||
ExitType,
|
ExitType,
|
||||||
|
MarginMode,
|
||||||
RunMode,
|
RunMode,
|
||||||
TradingMode,
|
TradingMode,
|
||||||
)
|
)
|
||||||
@@ -37,6 +38,7 @@ from freqtrade.exchange import (
|
|||||||
)
|
)
|
||||||
from freqtrade.exchange.exchange import Exchange
|
from freqtrade.exchange.exchange import Exchange
|
||||||
from freqtrade.ft_types import BacktestResultType, get_BacktestResultType_default
|
from freqtrade.ft_types import BacktestResultType, get_BacktestResultType_default
|
||||||
|
from freqtrade.leverage.liquidation_price import update_liquidation_prices
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
||||||
from freqtrade.optimize.bt_progress import BTProgress
|
from freqtrade.optimize.bt_progress import BTProgress
|
||||||
@@ -206,6 +208,7 @@ class Backtesting:
|
|||||||
self.required_startup = self.dataprovider.get_required_startup(self.timeframe)
|
self.required_startup = self.dataprovider.get_required_startup(self.timeframe)
|
||||||
|
|
||||||
self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
|
self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
|
||||||
|
self.margin_mode: MarginMode = config.get("margin_mode", MarginMode.ISOLATED)
|
||||||
# strategies which define "can_short=True" will fail to load in Spot mode.
|
# strategies which define "can_short=True" will fail to load in Spot mode.
|
||||||
self._can_short = self.trading_mode != TradingMode.SPOT
|
self._can_short = self.trading_mode != TradingMode.SPOT
|
||||||
self._position_stacking: bool = self.config.get("position_stacking", False)
|
self._position_stacking: bool = self.config.get("position_stacking", False)
|
||||||
@@ -698,21 +701,20 @@ class Backtesting:
|
|||||||
current_time=current_date,
|
current_time=current_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
|
if self.margin_mode == MarginMode.CROSS or not (
|
||||||
# trade is still open
|
order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount
|
||||||
trade.set_liquidation_price(
|
):
|
||||||
self.exchange.get_liquidation_price(
|
# trade is still open or we are in cross margin mode and
|
||||||
pair=trade.pair,
|
# must update all liquidation prices
|
||||||
open_rate=trade.open_rate,
|
update_liquidation_prices(
|
||||||
is_short=trade.is_short,
|
trade,
|
||||||
amount=trade.amount,
|
exchange=self.exchange,
|
||||||
stake_amount=trade.stake_amount,
|
wallets=self.wallets,
|
||||||
leverage=trade.leverage,
|
stake_currency=self.config["stake_currency"],
|
||||||
wallet_balance=trade.stake_amount,
|
dry_run=self.config["dry_run"],
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
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)
|
self._call_adjust_stop(current_date, trade, order.ft_price)
|
||||||
# pass
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -760,7 +760,7 @@ class LocalTrade:
|
|||||||
Method you should use to set self.liquidation price.
|
Method you should use to set self.liquidation price.
|
||||||
Assures stop_loss is not passed the liquidation price
|
Assures stop_loss is not passed the liquidation price
|
||||||
"""
|
"""
|
||||||
if not liquidation_price:
|
if liquidation_price is None:
|
||||||
return
|
return
|
||||||
self.liquidation_price = liquidation_price
|
self.liquidation_price = liquidation_price
|
||||||
|
|
||||||
|
|||||||
@@ -31,16 +31,6 @@ async def ui_version():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def is_relative_to(path: Path, base: Path) -> bool:
|
|
||||||
# Helper function simulating behaviour of is_relative_to, which was only added in python 3.9
|
|
||||||
try:
|
|
||||||
path.relative_to(base)
|
|
||||||
return True
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@router_ui.get("/{rest_of_path:path}", include_in_schema=False)
|
@router_ui.get("/{rest_of_path:path}", include_in_schema=False)
|
||||||
async def index_html(rest_of_path: str):
|
async def index_html(rest_of_path: str):
|
||||||
"""
|
"""
|
||||||
@@ -56,7 +46,7 @@ async def index_html(rest_of_path: str):
|
|||||||
if filename.suffix == ".js":
|
if filename.suffix == ".js":
|
||||||
# Force text/javascript for .js files - Circumvent faulty system configuration
|
# Force text/javascript for .js files - Circumvent faulty system configuration
|
||||||
media_type = "application/javascript"
|
media_type = "application/javascript"
|
||||||
if filename.is_file() and is_relative_to(filename, uibase):
|
if filename.is_file() and filename.is_relative_to(uibase):
|
||||||
return FileResponse(str(filename), media_type=media_type)
|
return FileResponse(str(filename), media_type=media_type)
|
||||||
|
|
||||||
index_file = uibase / "index.html"
|
index_file = uibase / "index.html"
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ authors = [
|
|||||||
|
|
||||||
description = "Freqtrade - Client scripts"
|
description = "Freqtrade - Client scripts"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.10"
|
||||||
license = {text = "GPLv3"}
|
license = {text = "GPLv3"}
|
||||||
# license = "GPLv3"
|
# license = "GPLv3"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: Science/Research",
|
"Intended Audience :: Science/Research",
|
||||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
|
|||||||
+1
-4
@@ -13,14 +13,12 @@ authors = [
|
|||||||
|
|
||||||
description = "Freqtrade - Crypto Trading Bot"
|
description = "Freqtrade - Crypto Trading Bot"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.10"
|
||||||
license = {text = "GPLv3"}
|
license = {text = "GPLv3"}
|
||||||
# license = "GPLv3"
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: Science/Research",
|
"Intended Audience :: Science/Research",
|
||||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
@@ -125,7 +123,6 @@ extend-exclude = [".env", ".venv"]
|
|||||||
target-version = "py38"
|
target-version = "py38"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
# Exclude UP036 as it's causing the "exit if < 3.9" to fail.
|
|
||||||
extend-select = [
|
extend-select = [
|
||||||
"C90", # mccabe
|
"C90", # mccabe
|
||||||
"B", # bugbear
|
"B", # bugbear
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.14.1; python_version >= "3.10"
|
scipy==1.14.1
|
||||||
scipy==1.13.1; python_version < "3.10"
|
|
||||||
scikit-learn==1.5.2
|
scikit-learn==1.5.2
|
||||||
ft-scikit-optimize==0.9.2
|
ft-scikit-optimize==0.9.2
|
||||||
filelock==3.16.1
|
filelock==3.16.1
|
||||||
|
|||||||
+1
-3
@@ -22,9 +22,7 @@ technical==1.4.4
|
|||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
pycoingecko==3.1.0
|
pycoingecko==3.1.0
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
# Tables 3.10 dropped support for Python 3.9
|
tables==3.10.1
|
||||||
tables==3.9.1; python_version < "3.10"
|
|
||||||
tables==3.10.1; python_version >= "3.10"
|
|
||||||
joblib==1.4.2
|
joblib==1.4.2
|
||||||
rich==13.8.1
|
rich==13.8.1
|
||||||
pyarrow==17.0.0; platform_machine != 'armv7l'
|
pyarrow==17.0.0; platform_machine != 'armv7l'
|
||||||
|
|||||||
@@ -153,16 +153,13 @@ function Find-PythonExecutable {
|
|||||||
"python3.12",
|
"python3.12",
|
||||||
"python3.11",
|
"python3.11",
|
||||||
"python3.10",
|
"python3.10",
|
||||||
"python3.9",
|
|
||||||
"python3",
|
"python3",
|
||||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python312\python.exe",
|
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python312\python.exe",
|
||||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python311\python.exe",
|
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python311\python.exe",
|
||||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe",
|
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe",
|
||||||
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python39\python.exe",
|
|
||||||
"C:\Python312\python.exe",
|
"C:\Python312\python.exe",
|
||||||
"C:\Python311\python.exe",
|
"C:\Python311\python.exe",
|
||||||
"C:\Python310\python.exe",
|
"C:\Python310\python.exe"
|
||||||
"C:\Python39\python.exe"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -178,10 +175,10 @@ function Main {
|
|||||||
"Starting the operations..." | Out-File $LogFilePath -Append
|
"Starting the operations..." | Out-File $LogFilePath -Append
|
||||||
"Current directory: $(Get-Location)" | Out-File $LogFilePath -Append
|
"Current directory: $(Get-Location)" | Out-File $LogFilePath -Append
|
||||||
|
|
||||||
# Exit on lower versions than Python 3.9 or when Python executable not found
|
# Exit on lower versions than Python 3.10 or when Python executable not found
|
||||||
$PythonExecutable = Find-PythonExecutable
|
$PythonExecutable = Find-PythonExecutable
|
||||||
if ($null -eq $PythonExecutable) {
|
if ($null -eq $PythonExecutable) {
|
||||||
Write-Log "No suitable Python executable found. Please ensure that Python 3.9 or higher is installed and available in the system PATH." -Level 'ERROR'
|
Write-Log "No suitable Python executable found. Please ensure that Python 3.10 or higher is installed and available in the system PATH." -Level 'ERROR'
|
||||||
Exit 1
|
Exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function check_installed_python() {
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for v in 12 11 10 9
|
for v in 12 11 10
|
||||||
do
|
do
|
||||||
PYTHON="python3.${v}"
|
PYTHON="python3.${v}"
|
||||||
which $PYTHON
|
which $PYTHON
|
||||||
@@ -36,7 +36,7 @@ function check_installed_python() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "No usable python found. Please make sure to have python3.9 or newer installed."
|
echo "No usable python found. Please make sure to have python3.10 or newer installed."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ function install_macos() {
|
|||||||
#Gets number after decimal in python version
|
#Gets number after decimal in python version
|
||||||
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
||||||
|
|
||||||
if [[ $version -ge 9 ]]; then #Checks if python version >= 3.9
|
if [[ $version -ge 10 ]]; then #Checks if python version >= 3.10
|
||||||
install_mac_newer_python_dependencies
|
install_mac_newer_python_dependencies
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -277,7 +277,7 @@ function install() {
|
|||||||
install_redhat
|
install_redhat
|
||||||
else
|
else
|
||||||
echo "This script does not support your OS."
|
echo "This script does not support your OS."
|
||||||
echo "If you have Python version 3.9 - 3.12, pip, virtualenv, ta-lib you can continue."
|
echo "If you have Python version 3.10 - 3.12, 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."
|
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
||||||
sleep 10
|
sleep 10
|
||||||
fi
|
fi
|
||||||
@@ -304,7 +304,7 @@ function help() {
|
|||||||
echo " -p,--plot Install dependencies for Plotting scripts."
|
echo " -p,--plot Install dependencies for Plotting scripts."
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verify if 3.9+ is installed
|
# Verify if 3.10+ is installed
|
||||||
check_installed_python
|
check_installed_python
|
||||||
|
|
||||||
case $* in
|
case $* in
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ def mock_trade_1(fee, is_short: bool):
|
|||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair="ETH/BTC",
|
pair="ETH/BTC",
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
amount=123.0,
|
amount=50.0,
|
||||||
amount_requested=123.0,
|
amount_requested=123.0,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
@@ -201,7 +201,7 @@ def mock_trade_4(fee, is_short: bool):
|
|||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair="ETC/BTC",
|
pair="ETC/BTC",
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
amount=123.0,
|
amount=0.0,
|
||||||
amount_requested=124.0,
|
amount_requested=124.0,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ def mock_trade_usdt_4(fee, is_short: bool):
|
|||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair="NEO/USDT",
|
pair="NEO/USDT",
|
||||||
stake_amount=20.0,
|
stake_amount=20.0,
|
||||||
amount=10.0,
|
amount=0.0,
|
||||||
amount_requested=10.01,
|
amount_requested=10.01,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import pytest
|
|||||||
|
|
||||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re
|
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
@@ -171,59 +172,101 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"is_short, trading_mode, margin_mode, wallet_balance, "
|
"pair, is_short, trading_mode, margin_mode, wallet_balance, "
|
||||||
"mm_ex_1, upnl_ex_1, maintenance_amt, amount, open_rate, "
|
"maintenance_amt, amount, open_rate, open_trades,"
|
||||||
"mm_ratio, expected",
|
"mm_ratio, expected",
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
|
"ETH/USDT:USDT",
|
||||||
False,
|
False,
|
||||||
"futures",
|
"futures",
|
||||||
"isolated",
|
"isolated",
|
||||||
1535443.01,
|
1535443.01,
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
135365.00,
|
135365.00,
|
||||||
3683.979,
|
3683.979,
|
||||||
1456.84,
|
1456.84,
|
||||||
|
[],
|
||||||
0.10,
|
0.10,
|
||||||
1114.78,
|
1114.78,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
"ETH/USDT:USDT",
|
||||||
False,
|
False,
|
||||||
"futures",
|
"futures",
|
||||||
"isolated",
|
"isolated",
|
||||||
1535443.01,
|
1535443.01,
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
16300.000,
|
16300.000,
|
||||||
109.488,
|
109.488,
|
||||||
32481.980,
|
32481.980,
|
||||||
|
[],
|
||||||
0.025,
|
0.025,
|
||||||
18778.73,
|
18778.73,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
"ETH/USDT:USDT",
|
||||||
False,
|
False,
|
||||||
"futures",
|
"futures",
|
||||||
"cross",
|
"cross",
|
||||||
1535443.01,
|
1535443.01,
|
||||||
71200.81144,
|
|
||||||
-56354.57,
|
|
||||||
135365.00,
|
135365.00,
|
||||||
3683.979,
|
3683.979, # amount
|
||||||
1456.84,
|
1456.84, # open_rate
|
||||||
|
[
|
||||||
|
{
|
||||||
|
# From calc example
|
||||||
|
"pair": "BTC/USDT:USDT",
|
||||||
|
"open_rate": 32481.98,
|
||||||
|
"amount": 109.488,
|
||||||
|
"stake_amount": 3556387.02624, # open_rate * amount
|
||||||
|
"mark_price": 31967.27,
|
||||||
|
"mm_ratio": 0.025,
|
||||||
|
"maintenance_amt": 16300.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# From calc example
|
||||||
|
"pair": "ETH/USDT:USDT",
|
||||||
|
"open_rate": 1456.84,
|
||||||
|
"amount": 3683.979,
|
||||||
|
"stake_amount": 5366967.96,
|
||||||
|
"mark_price": 1335.18,
|
||||||
|
"mm_ratio": 0.10,
|
||||||
|
"maintenance_amt": 135365.00,
|
||||||
|
},
|
||||||
|
],
|
||||||
0.10,
|
0.10,
|
||||||
1153.26,
|
1153.26,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
"BTC/USDT:USDT",
|
||||||
False,
|
False,
|
||||||
"futures",
|
"futures",
|
||||||
"cross",
|
"cross",
|
||||||
1535443.01,
|
1535443.01,
|
||||||
356512.508,
|
16300.0,
|
||||||
-448192.89,
|
109.488, # amount
|
||||||
16300.000,
|
32481.980, # open_rate
|
||||||
109.488,
|
[
|
||||||
32481.980,
|
{
|
||||||
|
# From calc example
|
||||||
|
"pair": "BTC/USDT:USDT",
|
||||||
|
"open_rate": 32481.98,
|
||||||
|
"amount": 109.488,
|
||||||
|
"stake_amount": 3556387.02624, # open_rate * amount
|
||||||
|
"mark_price": 31967.27,
|
||||||
|
"mm_ratio": 0.025,
|
||||||
|
"maintenance_amt": 16300.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# From calc example
|
||||||
|
"pair": "ETH/USDT:USDT",
|
||||||
|
"open_rate": 1456.84,
|
||||||
|
"amount": 3683.979,
|
||||||
|
"stake_amount": 5366967.96,
|
||||||
|
"mark_price": 1335.18,
|
||||||
|
"mm_ratio": 0.10,
|
||||||
|
"maintenance_amt": 135365.00,
|
||||||
|
},
|
||||||
|
],
|
||||||
0.025,
|
0.025,
|
||||||
26316.89,
|
26316.89,
|
||||||
),
|
),
|
||||||
@@ -232,15 +275,15 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
|
|||||||
def test_liquidation_price_binance(
|
def test_liquidation_price_binance(
|
||||||
mocker,
|
mocker,
|
||||||
default_conf,
|
default_conf,
|
||||||
open_rate,
|
pair,
|
||||||
is_short,
|
is_short,
|
||||||
trading_mode,
|
trading_mode,
|
||||||
margin_mode,
|
margin_mode,
|
||||||
wallet_balance,
|
wallet_balance,
|
||||||
mm_ex_1,
|
|
||||||
upnl_ex_1,
|
|
||||||
maintenance_amt,
|
maintenance_amt,
|
||||||
amount,
|
amount,
|
||||||
|
open_rate,
|
||||||
|
open_trades,
|
||||||
mm_ratio,
|
mm_ratio,
|
||||||
expected,
|
expected,
|
||||||
):
|
):
|
||||||
@@ -248,20 +291,48 @@ def test_liquidation_price_binance(
|
|||||||
default_conf["margin_mode"] = margin_mode
|
default_conf["margin_mode"] = margin_mode
|
||||||
default_conf["liquidation_buffer"] = 0.0
|
default_conf["liquidation_buffer"] = 0.0
|
||||||
exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
|
exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
|
||||||
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt))
|
|
||||||
|
def get_maint_ratio(pair_, stake_amount):
|
||||||
|
if pair_ != pair:
|
||||||
|
oc = [c for c in open_trades if c["pair"] == pair_][0]
|
||||||
|
return oc["mm_ratio"], oc["maintenance_amt"]
|
||||||
|
return mm_ratio, maintenance_amt
|
||||||
|
|
||||||
|
def fetch_funding_rates(*args, **kwargs):
|
||||||
|
return {
|
||||||
|
t["pair"]: {
|
||||||
|
"symbol": t["pair"],
|
||||||
|
"markPrice": t["mark_price"],
|
||||||
|
}
|
||||||
|
for t in open_trades
|
||||||
|
}
|
||||||
|
|
||||||
|
exchange.get_maintenance_ratio_and_amt = get_maint_ratio
|
||||||
|
exchange.fetch_funding_rates = fetch_funding_rates
|
||||||
|
|
||||||
|
open_trade_objects = [
|
||||||
|
Trade(
|
||||||
|
pair=t["pair"],
|
||||||
|
open_rate=t["open_rate"],
|
||||||
|
amount=t["amount"],
|
||||||
|
stake_amount=t["stake_amount"],
|
||||||
|
fee_open=0,
|
||||||
|
)
|
||||||
|
for t in open_trades
|
||||||
|
]
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
pytest.approx(
|
pytest.approx(
|
||||||
round(
|
round(
|
||||||
exchange.get_liquidation_price(
|
exchange.get_liquidation_price(
|
||||||
pair="DOGE/USDT",
|
pair=pair,
|
||||||
open_rate=open_rate,
|
open_rate=open_rate,
|
||||||
is_short=is_short,
|
is_short=is_short,
|
||||||
wallet_balance=wallet_balance,
|
wallet_balance=wallet_balance,
|
||||||
mm_ex_1=mm_ex_1,
|
|
||||||
upnl_ex_1=upnl_ex_1,
|
|
||||||
amount=amount,
|
amount=amount,
|
||||||
stake_amount=open_rate * amount,
|
stake_amount=open_rate * amount,
|
||||||
leverage=5,
|
leverage=5,
|
||||||
|
open_trades=open_trade_objects,
|
||||||
),
|
),
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5524,8 +5524,6 @@ def test_liquidation_price_is_none(
|
|||||||
stake_amount=open_rate * 71200.81144,
|
stake_amount=open_rate * 71200.81144,
|
||||||
leverage=5,
|
leverage=5,
|
||||||
wallet_balance=-56354.57,
|
wallet_balance=-56354.57,
|
||||||
mm_ex_1=0.10,
|
|
||||||
upnl_ex_1=0.0,
|
|
||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
@@ -6011,6 +6009,7 @@ def test_get_liquidation_price1(mocker, default_conf):
|
|||||||
stake_amount=18.884 * 0.8,
|
stake_amount=18.884 * 0.8,
|
||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
wallet_balance=18.884 * 0.8,
|
wallet_balance=18.884 * 0.8,
|
||||||
|
open_trades=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -6141,6 +6140,7 @@ def test_get_liquidation_price(
|
|||||||
wallet_balance=amount * open_rate / leverage,
|
wallet_balance=amount * open_rate / leverage,
|
||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
is_short=is_short,
|
is_short=is_short,
|
||||||
|
open_trades=[],
|
||||||
)
|
)
|
||||||
if expected_liq is None:
|
if expected_liq is None:
|
||||||
assert liq is None
|
assert liq is None
|
||||||
|
|||||||
@@ -457,6 +457,7 @@ class TestCCXTExchange:
|
|||||||
stake_amount=100,
|
stake_amount=100,
|
||||||
leverage=5,
|
leverage=5,
|
||||||
wallet_balance=100,
|
wallet_balance=100,
|
||||||
|
open_trades=[],
|
||||||
)
|
)
|
||||||
assert isinstance(liquidation_price, float)
|
assert isinstance(liquidation_price, float)
|
||||||
assert liquidation_price >= 0.0
|
assert liquidation_price >= 0.0
|
||||||
@@ -469,6 +470,7 @@ class TestCCXTExchange:
|
|||||||
stake_amount=100,
|
stake_amount=100,
|
||||||
leverage=5,
|
leverage=5,
|
||||||
wallet_balance=100,
|
wallet_balance=100,
|
||||||
|
open_trades=[],
|
||||||
)
|
)
|
||||||
assert isinstance(liquidation_price, float)
|
assert isinstance(liquidation_price, float)
|
||||||
assert liquidation_price >= 0.0
|
assert liquidation_price >= 0.0
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.enums.marginmode import MarginMode
|
||||||
|
from freqtrade.leverage.liquidation_price import update_liquidation_prices
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("dry_run", [False, True])
|
||||||
|
@pytest.mark.parametrize("margin_mode", [MarginMode.CROSS, MarginMode.ISOLATED])
|
||||||
|
def test_update_liquidation_prices(mocker, margin_mode, dry_run):
|
||||||
|
# Heavily mocked test - Only testing the logic of the function
|
||||||
|
# update liquidation price for trade in isolated mode
|
||||||
|
# update liquidation price for all trades in cross mode
|
||||||
|
exchange = MagicMock()
|
||||||
|
exchange.margin_mode = margin_mode
|
||||||
|
wallets = MagicMock()
|
||||||
|
trade_mock = MagicMock()
|
||||||
|
|
||||||
|
mocker.patch("freqtrade.persistence.Trade.get_open_trades", return_value=[trade_mock])
|
||||||
|
|
||||||
|
update_liquidation_prices(
|
||||||
|
trade=trade_mock,
|
||||||
|
exchange=exchange,
|
||||||
|
wallets=wallets,
|
||||||
|
stake_currency="USDT",
|
||||||
|
dry_run=dry_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert trade_mock.set_liquidation_price.call_count == 1
|
||||||
|
|
||||||
|
assert wallets.get_total.call_count == (
|
||||||
|
0 if margin_mode == MarginMode.ISOLATED or not dry_run else 1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with multiple trades
|
||||||
|
trade_mock.reset_mock()
|
||||||
|
trade_mock_2 = MagicMock()
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"freqtrade.persistence.Trade.get_open_trades", return_value=[trade_mock, trade_mock_2]
|
||||||
|
)
|
||||||
|
|
||||||
|
update_liquidation_prices(
|
||||||
|
trade=trade_mock,
|
||||||
|
exchange=exchange,
|
||||||
|
wallets=wallets,
|
||||||
|
stake_currency="USDT",
|
||||||
|
dry_run=dry_run,
|
||||||
|
)
|
||||||
|
# Trade2 is only updated in cross mode
|
||||||
|
assert trade_mock_2.set_liquidation_price.call_count == (
|
||||||
|
1 if margin_mode == MarginMode.CROSS else 0
|
||||||
|
)
|
||||||
|
assert trade_mock.set_liquidation_price.call_count == 1
|
||||||
|
|
||||||
|
assert wallets.call_count == 0 if not dry_run else 1
|
||||||
@@ -1269,7 +1269,7 @@ def test_api_mix_tag(botclient, fee):
|
|||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"is_short,current_rate,open_trade_value",
|
"is_short,current_rate,open_trade_value",
|
||||||
[(True, 1.098e-05, 15.0911775), (False, 1.099e-05, 15.1668225)],
|
[(True, 1.098e-05, 6.134625), (False, 1.099e-05, 6.165375)],
|
||||||
)
|
)
|
||||||
def test_api_status(
|
def test_api_status(
|
||||||
botclient, mocker, ticker, fee, markets, is_short, current_rate, open_trade_value
|
botclient, mocker, ticker, fee, markets, is_short, current_rate, open_trade_value
|
||||||
@@ -1294,7 +1294,7 @@ def test_api_status(
|
|||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert len(rc.json()) == 4
|
assert len(rc.json()) == 4
|
||||||
assert rc.json()[0] == {
|
assert rc.json()[0] == {
|
||||||
"amount": 123.0,
|
"amount": 50.0,
|
||||||
"amount_requested": 123.0,
|
"amount_requested": 123.0,
|
||||||
"close_date": None,
|
"close_date": None,
|
||||||
"close_timestamp": None,
|
"close_timestamp": None,
|
||||||
|
|||||||
@@ -362,7 +362,8 @@ def test_sync_wallet_dry(mocker, default_conf_usdt, fee):
|
|||||||
assert len(freqtrade.wallets._wallets) == 5
|
assert len(freqtrade.wallets._wallets) == 5
|
||||||
assert len(freqtrade.wallets._positions) == 0
|
assert len(freqtrade.wallets._positions) == 0
|
||||||
bal = freqtrade.wallets.get_all_balances()
|
bal = freqtrade.wallets.get_all_balances()
|
||||||
assert bal["NEO"].total == 10
|
# NEO trade is not filled yet.
|
||||||
|
assert bal["NEO"].total == 0
|
||||||
assert bal["XRP"].total == 10
|
assert bal["XRP"].total == 10
|
||||||
assert bal["LTC"].total == 2
|
assert bal["LTC"].total == 2
|
||||||
usdt_bal = bal["USDT"]
|
usdt_bal = bal["USDT"]
|
||||||
@@ -410,11 +411,11 @@ def test_sync_wallet_futures_dry(mocker, default_conf, fee):
|
|||||||
def test_check_exit_amount(mocker, default_conf, fee):
|
def test_check_exit_amount(mocker, default_conf, fee):
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
update_mock = mocker.patch("freqtrade.wallets.Wallets.update")
|
update_mock = mocker.patch("freqtrade.wallets.Wallets.update")
|
||||||
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=123)
|
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=50.0)
|
||||||
|
|
||||||
create_mock_trades(fee, is_short=None)
|
create_mock_trades(fee, is_short=None)
|
||||||
trade = Trade.session.scalars(select(Trade)).first()
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
assert trade.amount == 123
|
assert trade.amount == 50.0
|
||||||
|
|
||||||
assert freqtrade.wallets.check_exit_amount(trade) is True
|
assert freqtrade.wallets.check_exit_amount(trade) is True
|
||||||
assert update_mock.call_count == 0
|
assert update_mock.call_count == 0
|
||||||
@@ -423,7 +424,7 @@ def test_check_exit_amount(mocker, default_conf, fee):
|
|||||||
update_mock.reset_mock()
|
update_mock.reset_mock()
|
||||||
# Reduce returned amount to below the trade amount - which should
|
# Reduce returned amount to below the trade amount - which should
|
||||||
# trigger a wallet update and return False, triggering "order refinding"
|
# trigger a wallet update and return False, triggering "order refinding"
|
||||||
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=100)
|
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=40)
|
||||||
assert freqtrade.wallets.check_exit_amount(trade) is False
|
assert freqtrade.wallets.check_exit_amount(trade) is False
|
||||||
assert update_mock.call_count == 1
|
assert update_mock.call_count == 1
|
||||||
assert total_mock.call_count == 2
|
assert total_mock.call_count == 2
|
||||||
@@ -433,12 +434,12 @@ def test_check_exit_amount_futures(mocker, default_conf, fee):
|
|||||||
default_conf["trading_mode"] = "futures"
|
default_conf["trading_mode"] = "futures"
|
||||||
default_conf["margin_mode"] = "isolated"
|
default_conf["margin_mode"] = "isolated"
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=123)
|
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=50)
|
||||||
|
|
||||||
create_mock_trades(fee, is_short=None)
|
create_mock_trades(fee, is_short=None)
|
||||||
trade = Trade.session.scalars(select(Trade)).first()
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
trade.trading_mode = "futures"
|
trade.trading_mode = "futures"
|
||||||
assert trade.amount == 123
|
assert trade.amount == 50
|
||||||
|
|
||||||
assert freqtrade.wallets.check_exit_amount(trade) is True
|
assert freqtrade.wallets.check_exit_amount(trade) is True
|
||||||
assert total_mock.call_count == 0
|
assert total_mock.call_count == 0
|
||||||
|
|||||||
Reference in New Issue
Block a user