Merge pull request #12052 from freqtrade/new_release

New release 2025.7
This commit is contained in:
Matthias
2025-07-31 19:47:30 +02:00
committed by GitHub
135 changed files with 12901 additions and 9185 deletions
+5
View File
@@ -1,5 +1,10 @@
<!-- Thank you for sending your pull request. But first, have you included <!-- Thank you for sending your pull request. But first, have you included
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
Did you use AI to create your changes?
If so, please state it clearly in the PR description (failing to do so may result in your PR being closed).
Also, please do a self review of the changes made before submitting the PR to make sure only relevant changes are included.
--> -->
## Summary ## Summary
+7 -7
View File
@@ -25,7 +25,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ "ubuntu-22.04", "ubuntu-24.04" ] os: [ "ubuntu-22.04", "ubuntu-24.04" ]
python-version: ["3.10", "3.11", "3.12", "3.13"] python-version: ["3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -38,7 +38,7 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with: with:
activate-environment: true activate-environment: true
enable-cache: true enable-cache: true
@@ -160,7 +160,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ "macos-14", "macos-15" ] os: [ "macos-14", "macos-15" ]
python-version: ["3.10", "3.11", "3.12", "3.13"] python-version: ["3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -174,7 +174,7 @@ jobs:
check-latest: true check-latest: true
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with: with:
activate-environment: true activate-environment: true
enable-cache: true enable-cache: true
@@ -288,7 +288,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ windows-latest ] os: [ windows-latest ]
python-version: ["3.10", "3.11", "3.12", "3.13"] python-version: ["3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -301,7 +301,7 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with: with:
activate-environment: true activate-environment: true
enable-cache: true enable-cache: true
@@ -449,7 +449,7 @@ jobs:
python-version: "3.12" python-version: "3.12"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with: with:
activate-environment: true activate-environment: true
enable-cache: true enable-cache: true
-8
View File
@@ -55,14 +55,6 @@ jobs:
run: | run: |
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
# We need docker experimental to pull the ARM image.
- name: Switch docker to experimental
run: |
docker version -f '{{.Server.Experimental}}'
echo $'{\n "experimental": true\n}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker version -f '{{.Server.Experimental}}'
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
+29
View File
@@ -0,0 +1,29 @@
name: GitHub Actions Security Analysis with zizmor 🌈
on:
push:
branches:
- develop
- stable
pull_request:
branches:
- develop
- stable
permissions: {}
jobs:
zizmor:
runs-on: ubuntu-latest
permissions:
security-events: write
# contents: read # only needed for private repos
# actions: read # only needed for private repos
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@f52a838cfabf134edcbaa7c8b3677dde20045018 # v0.1.1
+6 -5
View File
@@ -21,16 +21,17 @@ repos:
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.16.1" rev: "v1.17.0"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
additional_dependencies: additional_dependencies:
- types-cachetools==6.0.0.20250525 - types-cachetools==6.1.0.20250717
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.32.4.20250611 - types-requests==2.32.4.20250611
- types-tabulate==0.9.0.20241207 - types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20250516 - types-python-dateutil==2.9.0.20250708
- scipy-stubs==1.16.0.2
- SQLAlchemy==2.0.41 - SQLAlchemy==2.0.41
# stages: [push] # stages: [push]
@@ -43,7 +44,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.12.1' rev: 'v0.12.5'
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format - id: ruff-format
@@ -69,7 +70,7 @@ repos:
)$ )$
- repo: https://github.com/stefmolin/exif-stripper - repo: https://github.com/stefmolin/exif-stripper
rev: 1.0.0 rev: 1.1.0
hooks: hooks:
- id: strip-exif - id: strip-exif
+9 -9
View File
@@ -1,10 +1,10 @@
FROM python:3.13.5-slim-bookworm as base FROM python:3.13.5-slim-bookworm AS base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG=C.UTF-8
ENV LC_ALL C.UTF-8 ENV LC_ALL=C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONFAULTHANDLER 1 ENV PYTHONFAULTHANDLER=1
ENV PATH=/home/ftuser/.local/bin:$PATH ENV PATH=/home/ftuser/.local/bin:$PATH
ENV FT_APP_ENV="docker" ENV FT_APP_ENV="docker"
@@ -21,7 +21,7 @@ RUN mkdir /freqtrade \
WORKDIR /freqtrade WORKDIR /freqtrade
# Install dependencies # Install dependencies
FROM base as python-deps FROM base AS python-deps
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \ && apt-get clean \
@@ -30,7 +30,7 @@ RUN apt-get update \
# Install TA-lib # Install TA-lib
COPY build_helpers/* /tmp/ COPY build_helpers/* /tmp/
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
ENV LD_LIBRARY_PATH /usr/local/lib ENV LD_LIBRARY_PATH=/usr/local/lib
# Install dependencies # Install dependencies
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
@@ -39,9 +39,9 @@ RUN pip install --user --no-cache-dir "numpy<3.0" \
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt && pip install --user --no-cache-dir -r requirements-hyperopt.txt
# Copy dependencies to runtime-image # Copy dependencies to runtime-image
FROM base as runtime-image FROM base AS runtime-image
COPY --from=python-deps /usr/local/lib /usr/local/lib COPY --from=python-deps /usr/local/lib /usr/local/lib
ENV LD_LIBRARY_PATH /usr/local/lib ENV LD_LIBRARY_PATH=/usr/local/lib
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
+5 -2
View File
@@ -64,7 +64,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
## Features ## Features
- [x] **Based on Python 3.10+**: For botting on any operating system - Windows, macOS and Linux. - [x] **Based on Python 3.11+**: 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.
@@ -146,6 +146,8 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
- `/stopentry`: Stop entering new trades. - `/stopentry`: Stop entering new trades.
- `/status <trade_id>|[table]`: Lists all or specific open trades. - `/status <trade_id>|[table]`: Lists all or specific open trades.
- `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days. - `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
- `/profit_long [<n>]`: Lists cumulative profit from all finished long trades, over the last n days.
- `/profit_short [<n>]`: Lists cumulative profit from all finished short trades, over the last n days.
- `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`). - `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`).
- `/fx <trade_id>|all`: Alias to `/forceexit` - `/fx <trade_id>|all`: Alias to `/forceexit`
- `/performance`: Show performance of each finished trade grouped by pair - `/performance`: Show performance of each finished trade grouped by pair
@@ -154,6 +156,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
- `/help`: Show help message. - `/help`: Show help message.
- `/version`: Show version. - `/version`: Show version.
## Development branches ## Development branches
The project is currently setup in two main branches: The project is currently setup in two main branches:
@@ -219,7 +222,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
### Software requirements ### Software requirements
- [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/) - [Python >= 3.11](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/)
+6 -4
View File
@@ -16,10 +16,12 @@ with require_dev.open("r") as rfile:
with require.open("r") as rfile: with require.open("r") as rfile:
requirements.extend(rfile.readlines()) requirements.extend(rfile.readlines())
# Extract types only # Extract relevant types only
type_reqs = [ supported = ("types-", "SQLAlchemy", "scipy-stubs")
r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy")
] # Find relevant dependencies
# Only keep the first part of the line up to the first space
type_reqs = [r.strip("\n").split()[0] for r in requirements if r.startswith(supported)]
with pre_commit_file.open("r") as file: with pre_commit_file.open("r") as file:
f = yaml.load(file, Loader=yaml.SafeLoader) f = yaml.load(file, Loader=yaml.SafeLoader)
+8 -8
View File
@@ -1,10 +1,10 @@
FROM python:3.11.13-slim-bookworm as base FROM python:3.11.13-slim-bookworm AS base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG=C.UTF-8
ENV LC_ALL C.UTF-8 ENV LC_ALL=C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONFAULTHANDLER 1 ENV PYTHONFAULTHANDLER=1
ENV PATH=/home/ftuser/.local/bin:$PATH ENV PATH=/home/ftuser/.local/bin:$PATH
ENV FT_APP_ENV="docker" ENV FT_APP_ENV="docker"
@@ -22,7 +22,7 @@ RUN mkdir /freqtrade \
WORKDIR /freqtrade WORKDIR /freqtrade
# Install dependencies # Install dependencies
FROM base as python-deps FROM base AS python-deps
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \ && apt-get clean \
@@ -39,9 +39,9 @@ RUN pip install --user --no-cache-dir "numpy<3.0" \
&& pip install --user --no-cache-dir -r requirements.txt && pip install --user --no-cache-dir -r requirements.txt
# Copy dependencies to runtime-image # Copy dependencies to runtime-image
FROM base as runtime-image FROM base AS runtime-image
COPY --from=python-deps /usr/local/lib /usr/local/lib COPY --from=python-deps /usr/local/lib /usr/local/lib
ENV LD_LIBRARY_PATH /usr/local/lib ENV LD_LIBRARY_PATH=/usr/local/lib
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
+4 -1
View File
@@ -321,6 +321,7 @@ It contains some useful key metrics about performance of your strategy on backte
| SQN | 2.45 | | SQN | 2.45 |
| Profit factor | 1.11 | | Profit factor | 1.11 |
| Expectancy (Ratio) | -0.15 (-0.05) | | Expectancy (Ratio) | -0.15 (-0.05) |
| Avg. daily profit | 0.0001 BTC |
| Avg. stake amount | 0.001 BTC | | Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC | | Total trade volume | 0.429 BTC |
| | | | | |
@@ -374,9 +375,11 @@ It contains some useful key metrics about performance of your strategy on backte
- `Calmar`: Annualized Calmar ratio. - `Calmar`: Annualized Calmar ratio.
- `SQN`: System Quality Number (SQN) - by Van Tharp. - `SQN`: System Quality Number (SQN) - by Van Tharp.
- `Profit factor`: profit / loss. - `Profit factor`: profit / loss.
- `Expectancy (Ratio)`: Expectancy ratio, which is the average profit or loss per trade. A negative expectancy ratio means that your strategy is not profitable.
- `Avg. daily profit`: Average profit per day, calculated as `(Total Profit / Backtest Days)`.
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Tot Profit %`. - `Best Pair` / `Worst Pair`: Best and worst performing pair (based on absolute profit), and it's corresponding `Tot Profit %`.
- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade. - `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade.
- `Best day` / `Worst day`: Best and worst day based on daily profit. - `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
+23 -2
View File
@@ -339,13 +339,13 @@ This needs to be configured like this:
```json ```json
"exchange": { "exchange": {
"name": "hyperliquid", "name": "hyperliquid",
"walletAddress": "your_eth_wallet_address", "walletAddress": "your_eth_wallet_address", // This should NOT be your API Wallet Address!
"privateKey": "your_api_private_key", "privateKey": "your_api_private_key",
// ... // ...
} }
``` ```
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your wallet address, not your API Wallet Address. * walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your main wallet address, not your API Wallet Address.
* privateKey in hex format: `0x<64 hex characters>` - Use the key the API Wallet shows on creation. * privateKey in hex format: `0x<64 hex characters>` - Use the key the API Wallet shows on creation.
Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed. Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed.
@@ -363,6 +363,27 @@ Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid. * Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid.
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet. * If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
### Hyperliquid Vault / Subaccount
Hyperliquid allows you to create either a vault or a subaccount.
To use these with Freqtrade, you will need to use the following configuration pattern:
``` json
"exchange": {
"name": "hyperliquid",
"walletAddress": "your_vault_address", // Vault or subaccount address
"privateKey": "your_api_private_key",
"ccxt_config": {
"options": {
"vaultAddress": "your_vault_address" // Optional, only if you want to use a vault or subaccount
}
},
// ...
}
```
Your balance and trades will now be used from your vault / subaccount - and no longer from your main account.
### Historic Hyperliquid data ### Historic Hyperliquid data
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data. The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
+8
View File
@@ -159,6 +159,14 @@ This warning can point to one of the below problems:
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling. * Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges). * API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
### I get the message "Couldn't reuse watch for xxx" in the log
This is an informational message that the bot tried to use candles from the websocket, but the exchange didn't provide the right information.
This can happen if there was an interruption to the websocket connection - or if the pair didn't have any trades happen in the timeframe you are using.
Freqtrade will handle this gracefully by falling back to the REST api.
While this makes the iteration slightly slower (due to the REST Api call) - it will not cause any problems to the bot's operation.
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy ### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Gate.io). As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Gate.io).
+1 -1
View File
@@ -87,7 +87,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
Alternatively Alternatively
- Python 3.10+ - Python 3.11+
- pip (pip3) - pip (pip3)
- git - git
- TA-Lib - TA-Lib
+4 -4
View File
@@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). 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.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. Python3.11 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.10](http://docs.python-guide.org/en/latest/starting/installation/) * [Python >= 3.11](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.10 or higher and the corresponding pip are assumed to be available. Python3.11 or higher and the corresponding pip are assumed to be available.
=== "Debian/Ubuntu" === "Debian/Ubuntu"
#### Install necessary dependencies #### Install necessary dependencies
@@ -179,7 +179,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.10+ installed beforehand for this to work. You will need to have git and python3.11+ 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/`
+1 -1
View File
@@ -1,6 +1,6 @@
markdown==3.8.2 markdown==3.8.2
mkdocs==1.6.1 mkdocs==1.6.1
mkdocs-material==9.6.14 mkdocs-material==9.6.16
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.16 pymdown-extensions==10.16
jinja2==3.1.6 jinja2==3.1.6
+37 -14
View File
@@ -174,17 +174,27 @@ class AwesomeStrategy(IStrategy):
## Enter Tag ## Enter Tag
When your strategy has multiple buy signals, you can name the signal that triggered. When your strategy has multiple entry signals, you can name the signal that triggered.
Then you can access your buy signal on `custom_exit` Then you can access your entry signal on `custom_exit`
```python ```python
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["enter_tag"] = ""
signal_rsi = (qtpylib.crossed_above(dataframe["rsi"], 35))
signal_bblower = (dataframe["bb_lowerband"] < dataframe["close"])
# Additional conditions
dataframe.loc[ dataframe.loc[
( (
(dataframe['rsi'] < 35) & signal_rsi
(dataframe['volume'] > 0) | signal_bblower
), # ... additional signals to enter a long position
['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi') )
& (dataframe["volume"] > 0)
, "enter_long"
] = 1
# Concatenate the tags so all signals are kept
dataframe.loc[signal_rsi, "enter_tag"] += "long_signal_rsi "
dataframe.loc[signal_bblower, "enter_tag"] += "long_signal_bblower "
return dataframe return dataframe
@@ -192,14 +202,17 @@ def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_r
current_profit: float, **kwargs): current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze() last_candle = dataframe.iloc[-1].squeeze()
if trade.enter_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80: if "long_signal_rsi" in trade.enter_tag and last_candle["rsi"] > 80:
return 'sell_signal_rsi' return "exit_signal_rsi"
if "long_signal_bblower" in trade.enter_tag and last_candle["high"] > last_candle["bb_upperband"]:
return "exit_signal_bblower"
# ...
return None return None
``` ```
!!! Note !!! Note
`enter_tag` is limited to 100 characters, remaining data will be truncated. `enter_tag` is limited to 255 characters, remaining data will be truncated.
!!! Warning !!! Warning
There is only one `enter_tag` column, which is used for both long and short trades. There is only one `enter_tag` column, which is used for both long and short trades.
@@ -213,17 +226,27 @@ Similar to [Entry Tagging](#enter-tag), you can also specify an exit tag.
``` python ``` python
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["exit_tag"] = ""
rsi_exit_signal = (dataframe["rsi"] > 70)
ema_exit_signal = (dataframe["ema20"] < dataframe["ema50"])
# Additional conditions
dataframe.loc[ dataframe.loc[
( (
(dataframe['rsi'] > 70) & rsi_exit_signal
(dataframe['volume'] > 0) | ema_exit_signal
), # ... additional signals to exit a long position
['exit_long', 'exit_tag']] = (1, 'exit_rsi') ) &
(dataframe["volume"] > 0)
,
"exit_long"] = 1
# Concatenate the tags so all signals are kept
dataframe.loc[rsi_exit_signal, "exit_tag"] += "exit_signal_rsi "
dataframe.loc[rsi_exit_signal2, "exit_tag"] += "exit_signal_rsi "
return dataframe return dataframe
``` ```
The provided exit-tag is then used as sell-reason - and shown as such in backtest results. The provided exit-tag is then used as exit-reason - and shown as such in backtest results.
!!! Note !!! Note
`exit_reason` is limited to 100 characters, remaining data will be truncated. `exit_reason` is limited to 100 characters, remaining data will be truncated.
+3
View File
@@ -229,6 +229,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade. | `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
| **Metrics** | | **Metrics** |
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
| `/profit_[long|short] [<n>]` | Display a summary of your profit/loss from close trades in one direction and some stats about your performance, over the last n days (all trades by default)
| `/performance` | Show performance of each finished trade grouped by pair | `/performance` | Show performance of each finished trade grouped by pair
| `/balance` | Show bot managed balance per currency | `/balance` | Show bot managed balance per currency
| `/balance full` | Show account balance per currency | `/balance full` | Show account balance per currency
@@ -309,6 +310,8 @@ current max
### /profit ### /profit
Also available as `/profit_long` and `/profit_short` to show profit for long or short trades only.
Return a summary of your profit/loss and performance. Return a summary of your profit/loss and performance.
> **ROI:** Close trades > **ROI:** Close trades
+9 -52
View File
@@ -117,9 +117,9 @@ Different payloads can be configured for different events. Not all fields are ne
## Webhook Message types ## Webhook Message types
### Entry ### Entry / Entry fill
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format. The fields in `webhook.entry` and `webhook.entry_fill` are filled when the bot places a long/short Order to increase a position, or when that order fills respectively. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@@ -162,31 +162,9 @@ Possible parameters are:
* `current_rate` * `current_rate`
* `enter_tag` * `enter_tag`
### Entry fill ### Exit / Exit fill
The fields in `webhook.entry_fill` are filled when the bot filled a long/short order. Parameters are filled using string.format. The fields in `webhook.exit` and `webhook.exit_fill` are filled when the bot places an exit order, or when that exit order fills respectively. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `open_rate`
* `amount`
* `open_date`
* `stake_amount`
* `stake_currency`
* `base_currency`
* `quote_currency`
* `fiat_currency`
* `order_type`
* `current_rate`
* `enter_tag`
### Exit
The fields in `webhook.exit` are filled when the bot exits a trade. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@@ -195,34 +173,9 @@ Possible parameters are:
* `direction` * `direction`
* `leverage` * `leverage`
* `gain` * `gain`
* `limit`
* `amount` * `amount`
* `open_rate` * `open_rate`
* `profit_amount`
* `profit_ratio`
* `stake_currency`
* `base_currency`
* `quote_currency`
* `fiat_currency`
* `exit_reason`
* `order_type`
* `open_date`
* `close_date`
### Exit fill
The fields in `webhook.exit_fill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `gain`
* `close_rate` * `close_rate`
* `amount`
* `open_rate`
* `current_rate` * `current_rate`
* `profit_amount` * `profit_amount`
* `profit_ratio` * `profit_ratio`
@@ -230,10 +183,14 @@ Possible parameters are:
* `base_currency` * `base_currency`
* `quote_currency` * `quote_currency`
* `fiat_currency` * `fiat_currency`
* `enter_tag`
* `exit_reason` * `exit_reason`
* `order_type` * `order_type`
* `open_date` * `open_date`
* `close_date` * `close_date`
* `sub_trade`
* `is_final_exit`
### Exit cancel ### Exit cancel
@@ -246,7 +203,7 @@ Possible parameters are:
* `direction` * `direction`
* `leverage` * `leverage`
* `gain` * `gain`
* `limit` * `order_rate`
* `amount` * `amount`
* `open_rate` * `open_rate`
* `current_rate` * `current_rate`
+2 -2
View File
@@ -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.10+ is installed and available. All instructions assume that python 3.11+ 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.10, 3.11, 3.12 and 3.13) 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.11, 3.12 and 3.13) 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.
+1 -1
View File
@@ -1,6 +1,6 @@
"""Freqtrade bot""" """Freqtrade bot"""
__version__ = "2025.6" __version__ = "2025.7"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path
+1 -1
View File
@@ -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.10) > python -m freqtrade (with Python >= 3.11)
""" """
from freqtrade import main from freqtrade import main
+13 -11
View File
@@ -258,24 +258,26 @@ ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs"
# Command level configs - keep at the bottom of the above definitions # Command level configs - keep at the bottom of the above definitions
NO_CONF_REQURIED = [ NO_CONF_REQURIED = [
"backtest-filter",
"backtesting-show",
"convert-data", "convert-data",
"convert-trade-data", "convert-trade-data",
"download-data", "download-data",
"list-timeframes", "hyperopt-list",
"hyperopt-show",
"list-data",
"list-freqaimodels",
"list-hyperoptloss",
"list-markets", "list-markets",
"list-pairs", "list-pairs",
"list-strategies", "list-strategies",
"list-freqaimodels", "list-timeframes",
"list-hyperoptloss",
"list-data",
"hyperopt-list",
"hyperopt-show",
"backtest-filter",
"plot-dataframe", "plot-dataframe",
"plot-profit", "plot-profit",
"show-trades", "show-trades",
"trades-to-ohlcv", "install-ui",
"strategy-updater", "strategy-updater",
"trades-to-ohlcv",
] ]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
@@ -311,8 +313,6 @@ class Arguments:
# (see https://bugs.python.org/issue16399) # (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting) # Allow no-config for certain commands (like downloading / plotting)
if "config" in parsed_arg and parsed_arg.config is None: if "config" in parsed_arg and parsed_arg.config is None:
conf_required = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None: if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None:
user_dir = parsed_arg.user_data_dir user_dir = parsed_arg.user_data_dir
else: else:
@@ -325,7 +325,9 @@ class Arguments:
else: else:
# Else use "config.json". # Else use "config.json".
cfgfile = Path.cwd() / DEFAULT_CONFIG cfgfile = Path.cwd() / DEFAULT_CONFIG
if cfgfile.is_file() or not conf_required: conf_optional = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
if cfgfile.is_file() or not conf_optional:
# Only inject config if the file exists, or if the config is required
parsed_arg.config = [DEFAULT_CONFIG] parsed_arg.config = [DEFAULT_CONFIG]
return parsed_arg return parsed_arg
-8
View File
@@ -18,10 +18,7 @@ from freqtrade.constants import Config
from freqtrade.enums import ( from freqtrade.enums import (
NON_UTIL_MODES, NON_UTIL_MODES,
TRADE_MODES, TRADE_MODES,
CandleType,
MarginMode,
RunMode, RunMode,
TradingMode,
) )
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.loggers import setup_logging from freqtrade.loggers import setup_logging
@@ -397,11 +394,6 @@ class Configuration:
self._args_to_config( self._args_to_config(
config, argname="trading_mode", logstring="Detected --trading-mode: {}" config, argname="trading_mode", logstring="Detected --trading-mode: {}"
) )
config["candle_type_def"] = CandleType.get_default(
config.get("trading_mode", "spot") or "spot"
)
config["trading_mode"] = TradingMode(config.get("trading_mode", "spot") or "spot")
config["margin_mode"] = MarginMode(config.get("margin_mode", "") or "")
self._args_to_config( self._args_to_config(
config, argname="candle_types", logstring="Detected --candle-types: {}" config, argname="candle_types", logstring="Detected --candle-types: {}"
) )
+4 -9
View File
@@ -4,9 +4,8 @@ This module contains the argument manager class
import logging import logging
import re import re
from datetime import datetime, timezone from datetime import UTC, datetime
from typing import Self
from typing_extensions import Self
from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.exceptions import ConfigurationError from freqtrade.exceptions import ConfigurationError
@@ -151,9 +150,7 @@ class TimeRange:
starts = rvals[index] starts = rvals[index]
if stype[0] == "date" and len(starts) == 8: if stype[0] == "date" and len(starts) == 8:
start = int( start = int(
datetime.strptime(starts, "%Y%m%d") datetime.strptime(starts, "%Y%m%d").replace(tzinfo=UTC).timestamp()
.replace(tzinfo=timezone.utc)
.timestamp()
) )
elif len(starts) == 13: elif len(starts) == 13:
start = int(starts) // 1000 start = int(starts) // 1000
@@ -164,9 +161,7 @@ class TimeRange:
stops = rvals[index] stops = rvals[index]
if stype[1] == "date" and len(stops) == 8: if stype[1] == "date" and len(stops) == 8:
stop = int( stop = int(
datetime.strptime(stops, "%Y%m%d") datetime.strptime(stops, "%Y%m%d").replace(tzinfo=UTC).timestamp()
.replace(tzinfo=timezone.utc)
.timestamp()
) )
elif len(stops) == 13: elif len(stops) == 13:
stop = int(stops) // 1000 stop = int(stops) // 1000
+2 -2
View File
@@ -5,7 +5,7 @@ Helpers when analyzing backtest data
import logging import logging
import zipfile import zipfile
from copy import copy from copy import copy
from datetime import datetime, timezone from datetime import UTC, datetime
from io import BytesIO, StringIO from io import BytesIO, StringIO
from pathlib import Path from pathlib import Path
from typing import Any, Literal from typing import Any, Literal
@@ -324,7 +324,7 @@ def find_existing_backtest_stats(
if min_backtest_date is not None: if min_backtest_date is not None:
backtest_date = strategy_metadata["backtest_start_time"] backtest_date = strategy_metadata["backtest_start_time"]
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc) backtest_date = datetime.fromtimestamp(backtest_date, tz=UTC)
if backtest_date < min_backtest_date: if backtest_date < min_backtest_date:
# Do not use a cached result for this strategy as first result is too old. # Do not use a cached result for this strategy as first result is too old.
del run_ids[strategy_name] del run_ids[strategy_name]
+8 -8
View File
@@ -7,7 +7,7 @@ Common Interface for bot and strategy to access data.
import logging import logging
from collections import deque from collections import deque
from datetime import datetime, timezone from datetime import UTC, datetime
from typing import Any from typing import Any
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
@@ -98,7 +98,7 @@ class DataProvider:
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
""" """
pair_key = (pair, timeframe, candle_type) pair_key = (pair, timeframe, candle_type)
self.__cached_pairs[pair_key] = (dataframe, datetime.now(timezone.utc)) self.__cached_pairs[pair_key] = (dataframe, datetime.now(UTC))
# For multiple producers we will want to merge the pairlists instead of overwriting # For multiple producers we will want to merge the pairlists instead of overwriting
def _set_producer_pairs(self, pairlist: list[str], producer_name: str = "default"): def _set_producer_pairs(self, pairlist: list[str], producer_name: str = "default"):
@@ -131,7 +131,7 @@ class DataProvider:
"data": { "data": {
"key": pair_key, "key": pair_key,
"df": dataframe.tail(1), "df": dataframe.tail(1),
"la": datetime.now(timezone.utc), "la": datetime.now(UTC),
}, },
} }
self.__rpc.send_msg(msg) self.__rpc.send_msg(msg)
@@ -164,7 +164,7 @@ class DataProvider:
if producer_name not in self.__producer_pairs_df: if producer_name not in self.__producer_pairs_df:
self.__producer_pairs_df[producer_name] = {} self.__producer_pairs_df[producer_name] = {}
_last_analyzed = datetime.now(timezone.utc) if not last_analyzed else last_analyzed _last_analyzed = datetime.now(UTC) if not last_analyzed else last_analyzed
self.__producer_pairs_df[producer_name][pair_key] = (dataframe, _last_analyzed) self.__producer_pairs_df[producer_name][pair_key] = (dataframe, _last_analyzed)
logger.debug(f"External DataFrame for {pair_key} from {producer_name} added.") logger.debug(f"External DataFrame for {pair_key} from {producer_name} added.")
@@ -275,12 +275,12 @@ class DataProvider:
# If we have no data from this Producer yet # If we have no data from this Producer yet
if producer_name not in self.__producer_pairs_df: if producer_name not in self.__producer_pairs_df:
# We don't have this data yet, return empty DataFrame and datetime (01-01-1970) # We don't have this data yet, return empty DataFrame and datetime (01-01-1970)
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
# If we do have data from that Producer, but no data on this pair_key # If we do have data from that Producer, but no data on this pair_key
if pair_key not in self.__producer_pairs_df[producer_name]: if pair_key not in self.__producer_pairs_df[producer_name]:
# We don't have this data yet, return empty DataFrame and datetime (01-01-1970) # We don't have this data yet, return empty DataFrame and datetime (01-01-1970)
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
# We have it, return this data # We have it, return this data
df, la = self.__producer_pairs_df[producer_name][pair_key] df, la = self.__producer_pairs_df[producer_name][pair_key]
@@ -396,10 +396,10 @@ class DataProvider:
if (max_index := self.__slice_index.get(pair)) is not None: if (max_index := self.__slice_index.get(pair)) is not None:
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index] df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
else: else:
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
return df, date return df, date
else: else:
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
@property @property
def runmode(self) -> RunMode: def runmode(self) -> RunMode:
@@ -8,7 +8,7 @@ import logging
import re import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
@@ -118,8 +118,8 @@ class IDataHandler(ABC):
df = self._ohlcv_load(pair, timeframe, None, candle_type) df = self._ohlcv_load(pair, timeframe, None, candle_type)
if df.empty: if df.empty:
return ( return (
datetime.fromtimestamp(0, tz=timezone.utc), datetime.fromtimestamp(0, tz=UTC),
datetime.fromtimestamp(0, tz=timezone.utc), datetime.fromtimestamp(0, tz=UTC),
0, 0,
) )
return df.iloc[0]["date"].to_pydatetime(), df.iloc[-1]["date"].to_pydatetime(), len(df) return df.iloc[0]["date"].to_pydatetime(), df.iloc[-1]["date"].to_pydatetime(), len(df)
@@ -201,8 +201,8 @@ class IDataHandler(ABC):
df = self._trades_load(pair, trading_mode) df = self._trades_load(pair, trading_mode)
if df.empty: if df.empty:
return ( return (
datetime.fromtimestamp(0, tz=timezone.utc), datetime.fromtimestamp(0, tz=UTC),
datetime.fromtimestamp(0, tz=timezone.utc), datetime.fromtimestamp(0, tz=UTC),
0, 0,
) )
return ( return (
+26 -4
View File
@@ -174,12 +174,18 @@ def calculate_underwater(
@dataclass() @dataclass()
class DrawDownResult: class DrawDownResult:
# Max drawdown fields
drawdown_abs: float = 0.0 drawdown_abs: float = 0.0
high_date: pd.Timestamp = None high_date: pd.Timestamp = None
low_date: pd.Timestamp = None low_date: pd.Timestamp = None
high_value: float = 0.0 high_value: float = 0.0
low_value: float = 0.0 low_value: float = 0.0
relative_account_drawdown: float = 0.0 relative_account_drawdown: float = 0.0
# Current drawdown fields
current_high_date: pd.Timestamp = None
current_high_value: float = 0.0
current_drawdown_abs: float = 0.0
current_relative_account_drawdown: float = 0.0
def calculate_max_drawdown( def calculate_max_drawdown(
@@ -191,29 +197,31 @@ def calculate_max_drawdown(
relative: bool = False, relative: bool = False,
) -> DrawDownResult: ) -> DrawDownResult:
""" """
Calculate max drawdown and the corresponding close dates Calculate max drawdown and current drawdown with corresponding dates
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio) :param trades: DataFrame containing trades (requires columns close_date and profit_abs)
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date') :param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
:param value_col: Column in DataFrame to use for values (defaults to 'profit_abs') :param value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
:param starting_balance: Portfolio starting balance - properly calculate relative drawdown. :param starting_balance: Portfolio starting balance - properly calculate relative drawdown.
:param relative: If True, use relative drawdown for max calculation instead of absolute
:return: DrawDownResult object :return: DrawDownResult object
with absolute max drawdown, high and low time and high and low value, with absolute max drawdown, high and low time and high and low value,
and the relative account drawdown relative account drawdown, and current drawdown information.
:raise: ValueError if trade-dataframe was found empty. :raise: ValueError if trade-dataframe was found empty.
""" """
if len(trades) == 0: if len(trades) == 0:
raise ValueError("Trade dataframe empty.") raise ValueError("Trade dataframe empty.")
profit_results = trades.sort_values(date_col).reset_index(drop=True) profit_results = trades.sort_values(date_col).reset_index(drop=True)
max_drawdown_df = _calc_drawdown_series( max_drawdown_df = _calc_drawdown_series(
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
) )
# Calculate maximum drawdown
idxmin = ( idxmin = (
max_drawdown_df["drawdown_relative"].idxmax() max_drawdown_df["drawdown_relative"].idxmax()
if relative if relative
else max_drawdown_df["drawdown"].idxmin() else max_drawdown_df["drawdown"].idxmin()
) )
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax() high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
high_date = profit_results.loc[high_idx, date_col] high_date = profit_results.loc[high_idx, date_col]
low_date = profit_results.loc[idxmin, date_col] low_date = profit_results.loc[idxmin, date_col]
@@ -221,13 +229,27 @@ def calculate_max_drawdown(
low_val = max_drawdown_df.loc[idxmin, "cumulative"] low_val = max_drawdown_df.loc[idxmin, "cumulative"]
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"] max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
# Calculate current drawdown
current_high_idx = max_drawdown_df["high_value"].iloc[:-1].idxmax()
current_high_date = profit_results.loc[current_high_idx, date_col]
current_high_value = max_drawdown_df.iloc[-1]["high_value"]
current_cumulative = max_drawdown_df.iloc[-1]["cumulative"]
current_drawdown_abs = current_high_value - current_cumulative
current_drawdown_relative = max_drawdown_df.iloc[-1]["drawdown_relative"]
return DrawDownResult( return DrawDownResult(
# Max drawdown
drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]), drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]),
high_date=high_date, high_date=high_date,
low_date=low_date, low_date=low_date,
high_value=high_val, high_value=high_val,
low_value=low_val, low_value=low_val,
relative_account_drawdown=max_drawdown_rel, relative_account_drawdown=max_drawdown_rel,
# Current drawdown
current_high_date=current_high_date,
current_high_value=current_high_value,
current_drawdown_abs=current_drawdown_abs,
current_relative_account_drawdown=current_drawdown_relative,
) )
+2
View File
@@ -43,4 +43,6 @@ from freqtrade.exchange.idex import Idex
from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.kucoin import Kucoin from freqtrade.exchange.kucoin import Kucoin
from freqtrade.exchange.lbank import Lbank from freqtrade.exchange.lbank import Lbank
from freqtrade.exchange.luno import Luno
from freqtrade.exchange.modetrade import Modetrade
from freqtrade.exchange.okx import Okx from freqtrade.exchange.okx import Okx
+4 -4
View File
@@ -1,7 +1,7 @@
"""Binance exchange subclass""" """Binance exchange subclass"""
import logging import logging
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
import ccxt import ccxt
@@ -63,7 +63,7 @@ class Binance(Exchange):
} }
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list (TradingMode.SPOT, MarginMode.NONE),
# (TradingMode.MARGIN, MarginMode.CROSS), # (TradingMode.MARGIN, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.CROSS), (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED), (TradingMode.FUTURES, MarginMode.ISOLATED),
@@ -160,7 +160,7 @@ class Binance(Exchange):
since_ms = x[3][0][0] since_ms = x[3][0][0]
logger.info( logger.info(
f"Candle-data for {pair} available starting with " f"Candle-data for {pair} available starting with "
f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}." f"{datetime.fromtimestamp(since_ms // 1000, tz=UTC).isoformat()}."
) )
if until_ms and since_ms >= until_ms: if until_ms and since_ms >= until_ms:
logger.warning( logger.warning(
@@ -399,7 +399,7 @@ class Binance(Exchange):
trades = await self._api_async.fetch_trades( trades = await self._api_async.fetch_trades(
pair, pair,
params={ params={
self._trades_pagination_arg: "0", self._ft_has["trades_pagination_arg"]: "0",
}, },
limit=5, limit=5,
) )
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,7 +1,7 @@
"""Bitpanda exchange subclass""" """Bitpanda exchange subclass"""
import logging import logging
from datetime import datetime, timezone from datetime import UTC, datetime
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@@ -34,5 +34,5 @@ class Bitpanda(Exchange):
:param pair: Pair the order is for :param pair: Pair the order is for
:param since: datetime object of the order creation time. Assumes object is in UTC. :param since: datetime object of the order creation time. Assumes object is in UTC.
""" """
params = {"to": int(datetime.now(timezone.utc).timestamp() * 1000)} params = {"to": int(datetime.now(UTC).timestamp() * 1000)}
return super().get_trades_for_order(order_id, pair, since, params) return super().get_trades_for_order(order_id, pair, since, params)
+31 -18
View File
@@ -1,8 +1,5 @@
"""Bybit exchange subclass"""
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any
import ccxt import ccxt
@@ -12,6 +9,7 @@ from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalExcep
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.misc import deep_merge_dicts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -64,9 +62,9 @@ class Bybit(Exchange):
} }
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list (TradingMode.SPOT, MarginMode.NONE),
(TradingMode.FUTURES, MarginMode.ISOLATED),
# (TradingMode.FUTURES, MarginMode.CROSS), # (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED)
] ]
@property @property
@@ -76,14 +74,11 @@ class Bybit(Exchange):
config = {} config = {}
if self.trading_mode == TradingMode.SPOT: if self.trading_mode == TradingMode.SPOT:
config.update({"options": {"defaultType": "spot"}}) config.update({"options": {"defaultType": "spot"}})
config.update(super()._ccxt_config) elif self.trading_mode == TradingMode.FUTURES:
config.update({"options": {"defaultSettle": self._config["stake_currency"]}})
config = deep_merge_dicts(config, super()._ccxt_config)
return config return config
def market_is_future(self, market: dict[str, Any]) -> bool:
main = super().market_is_future(market)
# For ByBit, we'll only support USDT markets for now.
return main and market["settle"] == "USDT"
@retrier @retrier
def additional_exchange_init(self) -> None: def additional_exchange_init(self) -> None:
""" """
@@ -182,18 +177,36 @@ class Bybit(Exchange):
PERPETUAL: PERPETUAL:
bybit: bybit:
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067 https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b USDT:
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
USDC:
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#c
Long: Long USDT:
Liquidation Price = (
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
- (Extra Margin Added/Contract Quantity))
Short USDT:
Liquidation Price = (
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
+ (Extra Margin Added/Contract Quantity))
Long USDC:
Liquidation Price = ( Liquidation Price = (
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity] Position Entry Price - [
- (Extra Margin Added/Contract Quantity)) (Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
Short: ]
)
Short USDC:
Liquidation Price = ( Liquidation Price = (
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity] Position Entry Price + [
+ (Extra Margin Added/Contract Quantity)) (Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
]
)
Implementation Note: Extra margin is currently not used. Implementation Note: Extra margin is currently not used.
Due to this - the liquidation formula between USDT and USDC is the same.
:param pair: Pair to calculate liquidation price for :param pair: Pair to calculate liquidation price for
:param open_rate: Entry price of position :param open_rate: Entry price of position
+77 -43
View File
@@ -9,7 +9,7 @@ import logging
import signal import signal
from collections.abc import Coroutine, Generator from collections.abc import Coroutine, Generator
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from math import floor, isnan from math import floor, isnan
from threading import Lock from threading import Lock
from typing import Any, Literal, TypeGuard, TypeVar from typing import Any, Literal, TypeGuard, TypeVar
@@ -137,6 +137,7 @@ class Exchange:
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv "ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
"ohlcv_partial_candle": True, "ohlcv_partial_candle": True,
"ohlcv_require_since": False, "ohlcv_require_since": False,
"always_require_api_keys": False, # purge API keys for Dry-run. Must default to false.
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
"ohlcv_volume_currency": "base", # "base" or "quote" "ohlcv_volume_currency": "base", # "base" or "quote"
"tickers_have_quoteVolume": True, "tickers_have_quoteVolume": True,
@@ -168,7 +169,8 @@ class Exchange:
_ft_has_futures: FtHas = {} _ft_has_futures: FtHas = {}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list # Non-defined exchanges only support spot mode.
(TradingMode.SPOT, MarginMode.NONE),
] ]
def __init__( def __init__(
@@ -197,7 +199,26 @@ class Exchange:
self.loop = self._init_async_loop() self.loop = self._init_async_loop()
self._config: Config = {} self._config: Config = {}
# Leverage properties
self.trading_mode: TradingMode = TradingMode(
config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
)
self.margin_mode: MarginMode = MarginMode(
MarginMode(config.get("margin_mode"))
if config.get("margin_mode")
else self._supported_trading_mode_margin_pairs[0][1]
)
config["trading_mode"] = self.trading_mode
config["margin_mode"] = self.margin_mode
config["candle_type_def"] = CandleType.get_default(self.trading_mode)
self._config.update(config) self._config.update(config)
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
exchange_conf: ExchangeConfig = exchange_config if exchange_config else config["exchange"]
# Deep merge ft_has with default ft_has options
# Must be called before ft_has is used.
self.build_ft_has(exchange_conf)
# Holds last candle refreshed time of each pair # Holds last candle refreshed time of each pair
self._pairs_last_refresh_time: dict[PairWithTimeframe, int] = {} self._pairs_last_refresh_time: dict[PairWithTimeframe, int] = {}
@@ -227,33 +248,17 @@ class Exchange:
if config["dry_run"]: if config["dry_run"]:
logger.info("Instance is running with dry_run enabled") logger.info("Instance is running with dry_run enabled")
logger.info(f"Using CCXT {ccxt.__version__}") logger.info(f"Using CCXT {ccxt.__version__}")
exchange_conf: dict[str, Any] = exchange_config if exchange_config else config["exchange"]
remove_exchange_credentials(exchange_conf, config.get("dry_run", False))
self.log_responses = exchange_conf.get("log_responses", False)
# Leverage properties # Don't remove exchange credentials for dry-run or if always_require_api_keys is set
self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT) remove_exchange_credentials(
self.margin_mode: MarginMode = ( exchange_conf,
MarginMode(config.get("margin_mode")) if config.get("margin_mode") else MarginMode.NONE not self._ft_has["always_require_api_keys"] and config.get("dry_run", False),
) )
self.liquidation_buffer = config.get("liquidation_buffer", 0.05) self.log_responses = exchange_conf.get("log_responses", False)
# Deep merge ft_has with default ft_has options
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
if self.trading_mode == TradingMode.FUTURES:
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
if exchange_conf.get("_ft_has_params"):
self._ft_has = deep_merge_dicts(exchange_conf.get("_ft_has_params"), self._ft_has)
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
# Assign this directly for easy access # Assign this directly for easy access
self._ohlcv_partial_candle = self._ft_has["ohlcv_partial_candle"] self._ohlcv_partial_candle = self._ft_has["ohlcv_partial_candle"]
self._max_trades_limit = self._ft_has["trades_limit"]
self._trades_pagination = self._ft_has["trades_pagination"]
self._trades_pagination_arg = self._ft_has["trades_pagination_arg"]
# Initialize ccxt objects # Initialize ccxt objects
ccxt_config = self._ccxt_config ccxt_config = self._ccxt_config
ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config) ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config)
@@ -289,10 +294,6 @@ class Exchange:
# Initial markets load # Initial markets load
self.reload_markets(True, load_leverage_tiers=False) self.reload_markets(True, load_leverage_tiers=False)
self.validate_config(config) self.validate_config(config)
self._startup_candle_count: int = config.get("startup_candle_count", 0)
self.required_candle_call_count = self.validate_required_startup_candles(
self._startup_candle_count, config.get("timeframe", "")
)
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers: if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
self.fill_leverage_tiers() self.fill_leverage_tiers()
@@ -331,6 +332,12 @@ class Exchange:
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
return loop return loop
def _set_startup_candle_count(self, config: Config) -> None:
self._startup_candle_count: int = config.get("startup_candle_count", 0)
self.required_candle_call_count = self.validate_required_startup_candles(
self._startup_candle_count, config.get("timeframe", "")
)
def validate_config(self, config: Config) -> None: def validate_config(self, config: Config) -> None:
# Check if timeframe is available # Check if timeframe is available
self.validate_timeframes(config.get("timeframe")) self.validate_timeframes(config.get("timeframe"))
@@ -345,6 +352,8 @@ class Exchange:
self.validate_orderflow(config["exchange"]) self.validate_orderflow(config["exchange"])
self.validate_freqai(config) self.validate_freqai(config)
self._set_startup_candle_count(config)
def _init_ccxt( def _init_ccxt(
self, exchange_config: dict[str, Any], sync: bool, ccxt_kwargs: dict[str, Any] self, exchange_config: dict[str, Any], sync: bool, ccxt_kwargs: dict[str, Any]
) -> ccxt.Exchange: ) -> ccxt.Exchange:
@@ -657,7 +666,7 @@ class Exchange:
if isinstance(markets, Exception): if isinstance(markets, Exception):
raise markets raise markets
return None return None
except asyncio.TimeoutError as e: except TimeoutError as e:
logger.warning("Could not load markets. Reason: %s", e) logger.warning("Could not load markets. Reason: %s", e)
raise TemporaryError from e raise TemporaryError from e
@@ -881,6 +890,20 @@ class Exchange:
f"Freqtrade does not support '{mm_value}' '{trading_mode}' on {self.name}." f"Freqtrade does not support '{mm_value}' '{trading_mode}' on {self.name}."
) )
def build_ft_has(self, exchange_conf: ExchangeConfig) -> None:
"""
Deep merge ft_has with default ft_has options
and with exchange_conf._ft_has_params if available.
This is called on initialization of the exchange object.
It must be called before ft_has is used.
"""
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
if self.trading_mode == TradingMode.FUTURES:
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
if exchange_conf.get("_ft_has_params"):
self._ft_has = deep_merge_dicts(exchange_conf.get("_ft_has_params"), self._ft_has)
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
def get_option(self, param: str, default: Any | None = None) -> Any: def get_option(self, param: str, default: Any | None = None) -> Any:
""" """
Get parameter value from _ft_has Get parameter value from _ft_has
@@ -2208,7 +2231,7 @@ class Exchange:
_params = params if params else {} _params = params if params else {}
my_trades = self._api.fetch_my_trades( my_trades = self._api.fetch_my_trades(
pair, pair,
int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000), int((since.replace(tzinfo=UTC).timestamp() - 5) * 1000),
params=_params, params=_params,
) )
matched_trades = [trade for trade in my_trades if trade["order"] == order_id] matched_trades = [trade for trade in my_trades if trade["order"] == order_id]
@@ -2584,10 +2607,12 @@ class Exchange:
if ticks and cache: if ticks and cache:
idx = -2 if drop_incomplete and len(ticks) > 1 else -1 idx = -2 if drop_incomplete and len(ticks) > 1 else -1
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0] self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0]
# keeping parsed dataframe in cache has_cache = cache and (pair, timeframe, c_type) in self._klines
# in case of existing cache, fill_missing happens after concatenation
ohlcv_df = ohlcv_to_dataframe( ohlcv_df = ohlcv_to_dataframe(
ticks, timeframe, pair=pair, fill_missing=True, drop_incomplete=drop_incomplete ticks, timeframe, pair=pair, fill_missing=not has_cache, drop_incomplete=drop_incomplete
) )
# keeping parsed dataframe in cache
if cache: if cache:
if (pair, timeframe, c_type) in self._klines: if (pair, timeframe, c_type) in self._klines:
old = self._klines[(pair, timeframe, c_type)] old = self._klines[(pair, timeframe, c_type)]
@@ -2995,7 +3020,7 @@ class Exchange:
returns: List of dicts containing trades, the next iteration value (new "since" or trade_id) returns: List of dicts containing trades, the next iteration value (new "since" or trade_id)
""" """
try: try:
trades_limit = self._max_trades_limit trades_limit = self._ft_has["trades_limit"]
# fetch trades asynchronously # fetch trades asynchronously
if params: if params:
logger.debug("Fetching trades for pair %s, params: %s ", pair, params) logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
@@ -3039,7 +3064,7 @@ class Exchange:
""" """
if not trades: if not trades:
return None return None
if self._trades_pagination == "id": if self._ft_has["trades_pagination"] == "id":
return trades[-1].get("id") return trades[-1].get("id")
else: else:
return trades[-1].get("timestamp") return trades[-1].get("timestamp")
@@ -3057,7 +3082,7 @@ class Exchange:
) -> tuple[str, list[list]]: ) -> tuple[str, list[list]]:
""" """
Asynchronously gets trade history using fetch_trades Asynchronously gets trade history using fetch_trades
use this when exchange uses id-based iteration (check `self._trades_pagination`) use this when exchange uses id-based iteration (check `self._ft_has["trades_pagination"]`)
:param pair: Pair to fetch trade data for :param pair: Pair to fetch trade data for
:param since: Since as integer timestamp in milliseconds :param since: Since as integer timestamp in milliseconds
:param until: Until as integer timestamp in milliseconds :param until: Until as integer timestamp in milliseconds
@@ -3083,7 +3108,7 @@ class Exchange:
while True: while True:
try: try:
t, from_id_next = await self._async_fetch_trades( t, from_id_next = await self._async_fetch_trades(
pair, params={self._trades_pagination_arg: from_id} pair, params={self._ft_has["trades_pagination_arg"]: from_id}
) )
if t: if t:
trades.extend(t[x]) trades.extend(t[x])
@@ -3111,7 +3136,7 @@ class Exchange:
) -> tuple[str, list[list]]: ) -> tuple[str, list[list]]:
""" """
Asynchronously gets trade history using fetch_trades, Asynchronously gets trade history using fetch_trades,
when the exchange uses time-based iteration (check `self._trades_pagination`) when the exchange uses time-based iteration (check `self._ft_has["trades_pagination"]`)
:param pair: Pair to fetch trade data for :param pair: Pair to fetch trade data for
:param since: Since as integer timestamp in milliseconds :param since: Since as integer timestamp in milliseconds
:param until: Until as integer timestamp in milliseconds :param until: Until as integer timestamp in milliseconds
@@ -3165,9 +3190,9 @@ class Exchange:
until = ccxt.Exchange.milliseconds() until = ccxt.Exchange.milliseconds()
logger.debug(f"Exchange milliseconds: {until}") logger.debug(f"Exchange milliseconds: {until}")
if self._trades_pagination == "time": if self._ft_has["trades_pagination"] == "time":
return await self._async_get_trade_history_time(pair=pair, since=since, until=until) return await self._async_get_trade_history_time(pair=pair, since=since, until=until)
elif self._trades_pagination == "id": elif self._ft_has["trades_pagination"] == "id":
return await self._async_get_trade_history_id( return await self._async_get_trade_history_id(
pair=pair, since=since, until=until, from_id=from_id pair=pair, since=since, until=until, from_id=from_id
) )
@@ -3335,7 +3360,7 @@ class Exchange:
if not filename.parent.is_dir(): if not filename.parent.is_dir():
filename.parent.mkdir(parents=True) filename.parent.mkdir(parents=True)
data = { data = {
"updated": datetime.now(timezone.utc), "updated": datetime.now(UTC),
"data": tiers, "data": tiers,
} }
file_dump_json(filename, data) file_dump_json(filename, data)
@@ -3357,7 +3382,7 @@ class Exchange:
updated = tiers.get("updated") updated = tiers.get("updated")
if updated: if updated:
updated_dt = parser.parse(updated) updated_dt = parser.parse(updated)
if updated_dt < datetime.now(timezone.utc) - cache_time: if updated_dt < datetime.now(UTC) - cache_time:
logger.info("Cached leverage tiers are outdated. Will update.") logger.info("Cached leverage tiers are outdated. Will update.")
return None return None
return tiers.get("data") return tiers.get("data")
@@ -3416,17 +3441,26 @@ class Exchange:
# Find the appropriate tier based on stake_amount # Find the appropriate tier based on stake_amount
prior_max_lev = None prior_max_lev = None
for tier in pair_tiers: for tier in pair_tiers:
# Adjust notional by leverage to do a proper comparison
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"]) min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
max_stake = tier["maxNotional"] / tier["maxLeverage"] max_stake = tier["maxNotional"] / tier["maxLeverage"]
prior_max_lev = tier["maxLeverage"] prior_max_lev = tier["maxLeverage"]
# Adjust notional by leverage to do a proper comparison
if min_stake <= stake_amount <= max_stake: if min_stake <= stake_amount <= max_stake:
return tier["maxLeverage"] return tier["maxLeverage"]
if stake_amount < min_stake and stake_amount <= max_stake:
# TODO: Remove this warning eventually
# Code could be simplified by removing the check for min-stake in the above
# condition, making this branch unnecessary.
logger.warning(
f"Fallback to next higher leverage tier for {pair}, stake: {stake_amount}, "
f"min_stake: {min_stake}."
)
return tier["maxLeverage"]
# else: # if on the last tier # else: # if on the last tier
if stake_amount > max_stake: if stake_amount > max_stake:
# If stake is > than max tradeable amount # If stake is > than max tradeable amount
raise InvalidOrderException(f"Amount {stake_amount} too high for {pair}") raise InvalidOrderException(f"Stake amount {stake_amount} too high for {pair}")
raise OperationalException( raise OperationalException(
f"Looped through all tiers without finding a max leverage for {pair}. " f"Looped through all tiers without finding a max leverage for {pair}. "
@@ -3572,7 +3606,7 @@ class Exchange:
mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"]) mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"])
if not close_date: if not close_date:
close_date = datetime.now(timezone.utc) close_date = datetime.now(UTC)
since_ms = dt_ts(timeframe_to_prev_date(timeframe, open_date)) since_ms = dt_ts(timeframe_to_prev_date(timeframe, open_date))
mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type) mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)
+1
View File
@@ -24,6 +24,7 @@ class FtHas(TypedDict, total=False):
ohlcv_require_since: bool ohlcv_require_since: bool
ohlcv_volume_currency: str ohlcv_volume_currency: str
ohlcv_candle_limit_per_timeframe: dict[str, int] ohlcv_candle_limit_per_timeframe: dict[str, int]
always_require_api_keys: bool
# Tickers # Tickers
tickers_have_quoteVolume: bool tickers_have_quoteVolume: bool
tickers_have_percentage: bool tickers_have_percentage: bool
+4 -4
View File
@@ -3,7 +3,7 @@ Exchange support utils
""" """
import inspect import inspect
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from math import ceil, floor, isnan from math import ceil, floor, isnan
from typing import Any from typing import Any
@@ -27,7 +27,7 @@ from freqtrade.exchange.common import (
SUPPORTED_EXCHANGES, SUPPORTED_EXCHANGES,
) )
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.ft_types import ValidExchangesType from freqtrade.ft_types import TradeModeType, ValidExchangesType
from freqtrade.util import FtPrecise from freqtrade.util import FtPrecise
@@ -110,7 +110,7 @@ def _build_exchange_list_entry(
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
} }
if resolved := exchangeClasses.get(mapped_exchange_name): if resolved := exchangeClasses.get(mapped_exchange_name):
supported_modes = [{"trading_mode": "spot", "margin_mode": ""}] + [ supported_modes: list[TradeModeType] = [
{"trading_mode": tm.value, "margin_mode": mm.value} {"trading_mode": tm.value, "margin_mode": mm.value}
for tm, mm in resolved["class"]._supported_trading_mode_margin_pairs for tm, mm in resolved["class"]._supported_trading_mode_margin_pairs
] ]
@@ -148,7 +148,7 @@ def date_minus_candles(timeframe: str, candle_count: int, date: datetime | None
""" """
if not date: if not date:
date = datetime.now(timezone.utc) date = datetime.now(UTC)
tf_min = timeframe_to_minutes(timeframe) tf_min = timeframe_to_minutes(timeframe)
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count) new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
@@ -1,4 +1,4 @@
from datetime import datetime, timezone from datetime import UTC, datetime
import ccxt import ccxt
from ccxt import ROUND_DOWN, ROUND_UP from ccxt import ROUND_DOWN, ROUND_UP
@@ -59,7 +59,7 @@ def timeframe_to_prev_date(timeframe: str, date: datetime | None = None) -> date
:returns: date of previous candle (with utc timezone) :returns: date of previous candle (with utc timezone)
""" """
if not date: if not date:
date = datetime.now(timezone.utc) date = datetime.now(UTC)
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000 new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000
return dt_from_ts(new_timestamp) return dt_from_ts(new_timestamp)
@@ -73,6 +73,6 @@ def timeframe_to_next_date(timeframe: str, date: datetime | None = None) -> date
:returns: date of next candle (with utc timezone) :returns: date of next candle (with utc timezone)
""" """
if not date: if not date:
date = datetime.now(timezone.utc) date = datetime.now(UTC)
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000 new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000
return dt_from_ts(new_timestamp) return dt_from_ts(new_timestamp)
+2 -2
View File
@@ -55,10 +55,10 @@ class Gate(Exchange):
} }
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list (TradingMode.SPOT, MarginMode.NONE),
# (TradingMode.MARGIN, MarginMode.CROSS), # (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS), # (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED) (TradingMode.FUTURES, MarginMode.ISOLATED),
] ]
@retrier @retrier
+3 -1
View File
@@ -28,6 +28,7 @@ class Hyperliquid(Exchange):
"stoploss_on_exchange": False, "stoploss_on_exchange": False,
"exchange_has_overrides": {"fetchTrades": False}, "exchange_has_overrides": {"fetchTrades": False},
"marketOrderRequiresPrice": True, "marketOrderRequiresPrice": True,
"ws_enabled": True,
} }
_ft_has_futures: FtHas = { _ft_has_futures: FtHas = {
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
@@ -40,7 +41,8 @@ class Hyperliquid(Exchange):
} }
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.FUTURES, MarginMode.ISOLATED) (TradingMode.SPOT, MarginMode.NONE),
(TradingMode.FUTURES, MarginMode.ISOLATED),
] ]
@property @property
+1 -1
View File
@@ -35,7 +35,7 @@ class Kraken(Exchange):
} }
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list (TradingMode.SPOT, MarginMode.NONE),
# (TradingMode.MARGIN, MarginMode.CROSS), # (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS) # (TradingMode.FUTURES, MarginMode.CROSS)
] ]
+24
View File
@@ -0,0 +1,24 @@
import logging
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
logger = logging.getLogger(__name__)
class Luno(Exchange):
"""
Luno exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: FtHas = {
"ohlcv_has_history": False, # Only provides the last 1000 candles
"always_require_api_keys": True, # Requires API keys to fetch candles
"trades_has_history": False, # Only the last 24h are available
}
+27
View File
@@ -0,0 +1,27 @@
import logging
# from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
logger = logging.getLogger(__name__)
class Modetrade(Exchange):
"""
MOdetrade exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: FtHas = {
"always_require_api_keys": True, # Requires API keys to fetch candles
}
# _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# (TradingMode.FUTURES, MarginMode.ISOLATED),
# ]
+1 -1
View File
@@ -49,7 +49,7 @@ class Okx(Exchange):
} }
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list (TradingMode.SPOT, MarginMode.NONE),
# (TradingMode.MARGIN, MarginMode.CROSS), # (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS), # (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED), (TradingMode.FUTURES, MarginMode.ISOLATED),
@@ -3,7 +3,7 @@ import importlib
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -239,7 +239,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
pair, refresh=False, side="exit", is_short=trade.is_short pair, refresh=False, side="exit", is_short=trade.is_short
) )
now = datetime.now(timezone.utc).timestamp() now = datetime.now(UTC).timestamp()
trade_duration = int((now - trade.open_date_utc.timestamp()) / self.base_tf_seconds) trade_duration = int((now - trade.open_date_utc.timestamp()) / self.base_tf_seconds)
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
if trade.is_short: if trade.is_short:
+2 -2
View File
@@ -5,7 +5,7 @@ import re
import shutil import shutil
import threading import threading
import warnings import warnings
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any, TypedDict from typing import Any, TypedDict
@@ -116,7 +116,7 @@ class FreqaiDataDrawer:
if metric not in self.metric_tracker[pair]: if metric not in self.metric_tracker[pair]:
self.metric_tracker[pair][metric] = {"timestamp": [], "value": []} self.metric_tracker[pair][metric] = {"timestamp": [], "value": []}
timestamp = int(datetime.now(timezone.utc).timestamp()) timestamp = int(datetime.now(UTC).timestamp())
self.metric_tracker[pair][metric]["value"].append(value) self.metric_tracker[pair][metric]["value"].append(value)
self.metric_tracker[pair][metric]["timestamp"].append(timestamp) self.metric_tracker[pair][metric]["timestamp"].append(timestamp)
+4 -4
View File
@@ -3,7 +3,7 @@ import inspect
import logging import logging
import random import random
import shutil import shutil
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -341,7 +341,7 @@ class FreqaiDataKitchen:
full_timerange = TimeRange.parse_timerange(tr) full_timerange = TimeRange.parse_timerange(tr)
config_timerange = TimeRange.parse_timerange(self.config["timerange"]) config_timerange = TimeRange.parse_timerange(self.config["timerange"])
if config_timerange.stopts == 0: if config_timerange.stopts == 0:
config_timerange.stopts = int(datetime.now(tz=timezone.utc).timestamp()) config_timerange.stopts = int(datetime.now(tz=UTC).timestamp())
timerange_train = copy.deepcopy(full_timerange) timerange_train = copy.deepcopy(full_timerange)
timerange_backtest = copy.deepcopy(full_timerange) timerange_backtest = copy.deepcopy(full_timerange)
@@ -525,7 +525,7 @@ class FreqaiDataKitchen:
:return: :return:
bool = If the model is expired or not. bool = If the model is expired or not.
""" """
time = datetime.now(tz=timezone.utc).timestamp() time = datetime.now(tz=UTC).timestamp()
elapsed_time = (time - trained_timestamp) / 3600 # hours elapsed_time = (time - trained_timestamp) / 3600 # hours
max_time = self.freqai_config.get("expiration_hours", 0) max_time = self.freqai_config.get("expiration_hours", 0)
if max_time > 0: if max_time > 0:
@@ -536,7 +536,7 @@ class FreqaiDataKitchen:
def check_if_new_training_required( def check_if_new_training_required(
self, trained_timestamp: int self, trained_timestamp: int
) -> tuple[bool, TimeRange, TimeRange]: ) -> tuple[bool, TimeRange, TimeRange]:
time = datetime.now(tz=timezone.utc).timestamp() time = datetime.now(tz=UTC).timestamp()
trained_timerange = TimeRange() trained_timerange = TimeRange()
data_load_timerange = TimeRange() data_load_timerange = TimeRange()
+2 -2
View File
@@ -3,7 +3,7 @@ import threading
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import deque from collections import deque
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any, Literal from typing import Any, Literal
@@ -76,7 +76,7 @@ class IFreqaiModel(ABC):
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config) self.dd = FreqaiDataDrawer(Path(self.full_path), self.config)
# set current candle to arbitrary historical date # set current candle to arbitrary historical date
self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=timezone.utc) self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=UTC)
self.dd.current_candle = self.current_candle self.dd.current_candle = self.current_candle
self.scanning = False self.scanning = False
self.ft_params = self.freqai_info["feature_parameters"] self.ft_params = self.freqai_info["feature_parameters"]
+2 -2
View File
@@ -1,5 +1,5 @@
import logging import logging
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -64,7 +64,7 @@ def get_required_data_timerange(config: Config) -> TimeRange:
Used to compute the required data download time range Used to compute the required data download time range
for auto data-download in FreqAI for auto data-download in FreqAI
""" """
time = datetime.now(tz=timezone.utc).timestamp() time = datetime.now(tz=UTC).timestamp()
timeframes = config["freqai"]["feature_parameters"].get("include_timeframes") timeframes = config["freqai"]["feature_parameters"].get("include_timeframes")
+76 -73
View File
@@ -5,7 +5,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
import logging import logging
import traceback import traceback
from copy import deepcopy from copy import deepcopy
from datetime import datetime, time, timedelta, timezone from datetime import UTC, datetime, time, timedelta
from math import isclose from math import isclose
from threading import Lock from threading import Lock
from time import sleep from time import sleep
@@ -93,14 +93,16 @@ class FreqtradeBot(LoggingMixin):
# Remove credentials from original exchange config to avoid accidental credential exposure # Remove credentials from original exchange config to avoid accidental credential exposure
remove_exchange_credentials(config["exchange"], True) remove_exchange_credentials(config["exchange"], True)
self.exchange = ExchangeResolver.load_exchange(
self.config, exchange_config=exchange_config, load_leverage_tiers=True
)
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
# Check config consistency here since strategies can set certain options # Check config consistency here since strategies can set certain options
validate_config_consistency(config) validate_config_consistency(config)
# Re-validate exchange compatibility
self.exchange = ExchangeResolver.load_exchange( self.exchange.validate_config(self.config)
self.config, exchange_config=exchange_config, load_leverage_tiers=True
)
init_db(self.config["db_url"]) init_db(self.config["db_url"])
@@ -266,7 +268,7 @@ class FreqtradeBot(LoggingMixin):
) )
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)( strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
current_time=datetime.now(timezone.utc) current_time=datetime.now(UTC)
) )
with self._measure_execution: with self._measure_execution:
@@ -296,7 +298,7 @@ class FreqtradeBot(LoggingMixin):
self._schedule.run_pending() self._schedule.run_pending()
Trade.commit() Trade.commit()
self.rpc.process_msg_queue(self.dataprovider._msg_queue) self.rpc.process_msg_queue(self.dataprovider._msg_queue)
self.last_process = datetime.now(timezone.utc) self.last_process = datetime.now(UTC)
def process_stopped(self) -> None: def process_stopped(self) -> None:
""" """
@@ -421,7 +423,7 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException as e: except InvalidOrderException as e:
logger.warning(f"Error updating Order {order.order_id} due to {e}.") logger.warning(f"Error updating Order {order.order_id} due to {e}.")
if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc): if order.order_date_utc - timedelta(days=5) < datetime.now(UTC):
logger.warning( logger.warning(
"Order is older than 5 days. Assuming order was fully cancelled." "Order is older than 5 days. Assuming order was fully cancelled."
) )
@@ -755,7 +757,7 @@ class FreqtradeBot(LoggingMixin):
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
stake_amount, order_tag = self.strategy._adjust_trade_position_internal( stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
trade=trade, trade=trade,
current_time=datetime.now(timezone.utc), current_time=datetime.now(UTC),
current_rate=current_entry_rate, current_rate=current_entry_rate,
current_profit=current_entry_profit, current_profit=current_entry_profit,
min_stake=min_entry_stake, min_stake=min_entry_stake,
@@ -916,7 +918,7 @@ class FreqtradeBot(LoggingMixin):
amount=amount, amount=amount,
rate=enter_limit_requested, rate=enter_limit_requested,
time_in_force=time_in_force, time_in_force=time_in_force,
current_time=datetime.now(timezone.utc), current_time=datetime.now(UTC),
entry_tag=enter_tag, entry_tag=enter_tag,
side=trade_side, side=trade_side,
): ):
@@ -987,7 +989,7 @@ class FreqtradeBot(LoggingMixin):
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker="maker") fee = self.exchange.get_fee(symbol=pair, taker_or_maker="maker")
base_currency = self.exchange.get_pair_base_currency(pair) base_currency = self.exchange.get_pair_base_currency(pair)
open_date = datetime.now(timezone.utc) open_date = datetime.now(UTC)
funding_fees = self.exchange.get_funding_fees( funding_fees = self.exchange.get_funding_fees(
pair=pair, pair=pair,
@@ -1106,7 +1108,7 @@ class FreqtradeBot(LoggingMixin):
)( )(
pair=pair, pair=pair,
trade=trade, trade=trade,
current_time=datetime.now(timezone.utc), current_time=datetime.now(UTC),
proposed_rate=enter_limit_requested, proposed_rate=enter_limit_requested,
entry_tag=entry_tag, entry_tag=entry_tag,
side=trade_side, side=trade_side,
@@ -1124,7 +1126,7 @@ class FreqtradeBot(LoggingMixin):
else: else:
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair, pair=pair,
current_time=datetime.now(timezone.utc), current_time=datetime.now(UTC),
current_rate=enter_limit_requested, current_rate=enter_limit_requested,
proposed_leverage=1.0, proposed_leverage=1.0,
max_leverage=max_leverage, max_leverage=max_leverage,
@@ -1157,7 +1159,7 @@ class FreqtradeBot(LoggingMixin):
self.strategy.custom_stake_amount, default_retval=stake_amount self.strategy.custom_stake_amount, default_retval=stake_amount
)( )(
pair=pair, pair=pair,
current_time=datetime.now(timezone.utc), current_time=datetime.now(UTC),
current_rate=enter_limit_requested, current_rate=enter_limit_requested,
proposed_stake=stake_amount, proposed_stake=stake_amount,
min_stake=min_stake_amount, min_stake=min_stake_amount,
@@ -1214,6 +1216,7 @@ class FreqtradeBot(LoggingMixin):
"leverage": trade.leverage if trade.leverage else None, "leverage": trade.leverage if trade.leverage else None,
"direction": "Short" if trade.is_short else "Long", "direction": "Short" if trade.is_short else "Long",
"limit": open_rate, # Deprecated (?) "limit": open_rate, # Deprecated (?)
"order_rate": open_rate,
"open_rate": open_rate, "open_rate": open_rate,
"order_type": order_type or "unknown", "order_type": order_type or "unknown",
"stake_amount": stake_amount, "stake_amount": stake_amount,
@@ -1222,7 +1225,7 @@ class FreqtradeBot(LoggingMixin):
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair), "quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
"fiat_currency": self.config.get("fiat_display_currency", None), "fiat_currency": self.config.get("fiat_display_currency", None),
"amount": order.safe_amount_after_fee if fill else (order.safe_amount or trade.amount), "amount": order.safe_amount_after_fee if fill else (order.safe_amount or trade.amount),
"open_date": trade.open_date_utc or datetime.now(timezone.utc), "open_date": trade.open_date_utc or datetime.now(UTC),
"current_rate": current_rate, "current_rate": current_rate,
"sub_trade": sub_trade, "sub_trade": sub_trade,
} }
@@ -1250,6 +1253,7 @@ class FreqtradeBot(LoggingMixin):
"leverage": trade.leverage, "leverage": trade.leverage,
"direction": "Short" if trade.is_short else "Long", "direction": "Short" if trade.is_short else "Long",
"limit": trade.open_rate, "limit": trade.open_rate,
"order_rate": trade.open_rate,
"order_type": order_type, "order_type": order_type,
"stake_amount": trade.stake_amount, "stake_amount": trade.stake_amount,
"open_rate": trade.open_rate, "open_rate": trade.open_rate,
@@ -1361,7 +1365,7 @@ class FreqtradeBot(LoggingMixin):
exits: list[ExitCheckTuple] = self.strategy.should_exit( exits: list[ExitCheckTuple] = self.strategy.should_exit(
trade, trade,
exit_rate, exit_rate,
datetime.now(timezone.utc), datetime.now(UTC),
enter=enter, enter=enter,
exit_=exit_, exit_=exit_,
force_stoploss=0, force_stoploss=0,
@@ -1479,44 +1483,6 @@ class FreqtradeBot(LoggingMixin):
return False return False
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: CcxtOrder) -> None:
"""
Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange
:param trade: Corresponding Trade
:param order: Current on exchange stoploss order
:return: None
"""
stoploss_norm = self.exchange.price_to_precision(
trade.pair,
trade.stoploss_or_liquidation,
rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP,
)
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
# we check if the update is necessary
update_beat = self.strategy.order_types.get("stoploss_on_exchange_interval", 60)
upd_req = datetime.now(timezone.utc) - timedelta(seconds=update_beat)
if trade.stoploss_last_update_utc and upd_req >= trade.stoploss_last_update_utc:
# cancelling the current stoploss on exchange first
logger.info(
f"Cancelling current stoploss on exchange for pair {trade.pair} "
f"(orderid:{order['id']}) in order to add another one ..."
)
self.cancel_stoploss_on_exchange(trade)
if not trade.is_open:
logger.warning(
f"Trade {trade} is closed, not creating trailing stoploss order."
)
return
# Create new stoploss order
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
logger.warning(
f"Could not create trailing stoploss order for pair {trade.pair}."
)
def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: list[CcxtOrder]): def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: list[CcxtOrder]):
""" """
Perform required actions according to existing stoploss orders of trade Perform required actions according to existing stoploss orders of trade
@@ -1558,6 +1524,44 @@ class FreqtradeBot(LoggingMixin):
return return
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: CcxtOrder) -> None:
"""
Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange
:param trade: Corresponding Trade
:param order: Current on exchange stoploss order
:return: None
"""
stoploss_norm = self.exchange.price_to_precision(
trade.pair,
trade.stoploss_or_liquidation,
rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP,
)
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
# we check if the update is necessary
update_beat = self.strategy.order_types.get("stoploss_on_exchange_interval", 60)
upd_req = datetime.now(UTC) - timedelta(seconds=update_beat)
if trade.stoploss_last_update_utc and upd_req >= trade.stoploss_last_update_utc:
# cancelling the current stoploss on exchange first
logger.info(
f"Cancelling current stoploss on exchange for pair {trade.pair} "
f"(orderid:{order['id']}) in order to add another one ..."
)
self.cancel_stoploss_on_exchange(trade)
if not trade.is_open:
logger.warning(
f"Trade {trade} is closed, not creating trailing stoploss order."
)
return
# Create new stoploss order
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
logger.warning(
f"Could not create trailing stoploss order for pair {trade.pair}."
)
def manage_open_orders(self) -> None: def manage_open_orders(self) -> None:
""" """
Management of open orders on exchange. Unfilled orders might be cancelled if timeout Management of open orders on exchange. Unfilled orders might be cancelled if timeout
@@ -1583,9 +1587,7 @@ class FreqtradeBot(LoggingMixin):
if not_closed: if not_closed:
if fully_cancelled or ( if fully_cancelled or (
open_order open_order
and self.strategy.ft_check_timed_out( and self.strategy.ft_check_timed_out(trade, open_order, datetime.now(UTC))
trade, open_order, datetime.now(timezone.utc)
)
): ):
self.handle_cancel_order( self.handle_cancel_order(
order, open_order, trade, constants.CANCEL_REASON["TIMEOUT"] order, open_order, trade, constants.CANCEL_REASON["TIMEOUT"]
@@ -1683,7 +1685,7 @@ class FreqtradeBot(LoggingMixin):
trade=trade, trade=trade,
order=order_obj, order=order_obj,
pair=trade.pair, pair=trade.pair,
current_time=datetime.now(timezone.utc), current_time=datetime.now(UTC),
proposed_rate=proposed_rate, proposed_rate=proposed_rate,
current_order_rate=order_obj.safe_placement_price, current_order_rate=order_obj.safe_placement_price,
entry_tag=trade.enter_tag, entry_tag=trade.enter_tag,
@@ -2075,7 +2077,7 @@ class FreqtradeBot(LoggingMixin):
)( )(
pair=trade.pair, pair=trade.pair,
trade=trade, trade=trade,
current_time=datetime.now(timezone.utc), current_time=datetime.now(UTC),
proposed_rate=proposed_limit_rate, proposed_rate=proposed_limit_rate,
current_profit=current_profit, current_profit=current_profit,
exit_tag=exit_reason, exit_tag=exit_reason,
@@ -2106,7 +2108,7 @@ class FreqtradeBot(LoggingMixin):
time_in_force=time_in_force, time_in_force=time_in_force,
exit_reason=exit_reason, exit_reason=exit_reason,
sell_reason=exit_reason, # sellreason -> compatibility sell_reason=exit_reason, # sellreason -> compatibility
current_time=datetime.now(timezone.utc), current_time=datetime.now(UTC),
) )
): ):
logger.info(f"User denied exit for {trade.pair}.") logger.info(f"User denied exit for {trade.pair}.")
@@ -2202,7 +2204,7 @@ class FreqtradeBot(LoggingMixin):
"enter_tag": trade.enter_tag, "enter_tag": trade.enter_tag,
"exit_reason": trade.exit_reason, "exit_reason": trade.exit_reason,
"open_date": trade.open_date_utc, "open_date": trade.open_date_utc,
"close_date": trade.close_date_utc or datetime.now(timezone.utc), "close_date": trade.close_date_utc or datetime.now(UTC),
"stake_amount": trade.stake_amount, "stake_amount": trade.stake_amount,
"stake_currency": self.config["stake_currency"], "stake_currency": self.config["stake_currency"],
"base_currency": self.exchange.get_pair_base_currency(trade.pair), "base_currency": self.exchange.get_pair_base_currency(trade.pair),
@@ -2247,6 +2249,7 @@ class FreqtradeBot(LoggingMixin):
"direction": "Short" if trade.is_short else "Long", "direction": "Short" if trade.is_short else "Long",
"gain": gain, "gain": gain,
"limit": profit_rate or 0, "limit": profit_rate or 0,
"order_rate": profit_rate or 0,
"order_type": order_type, "order_type": order_type,
"amount": order.safe_amount_after_fee, "amount": order.safe_amount_after_fee,
"open_rate": trade.open_rate, "open_rate": trade.open_rate,
@@ -2257,7 +2260,7 @@ class FreqtradeBot(LoggingMixin):
"enter_tag": trade.enter_tag, "enter_tag": trade.enter_tag,
"exit_reason": trade.exit_reason, "exit_reason": trade.exit_reason,
"open_date": trade.open_date, "open_date": trade.open_date,
"close_date": trade.close_date or datetime.now(timezone.utc), "close_date": trade.close_date or datetime.now(UTC),
"stake_currency": self.config["stake_currency"], "stake_currency": self.config["stake_currency"],
"base_currency": self.exchange.get_pair_base_currency(trade.pair), "base_currency": self.exchange.get_pair_base_currency(trade.pair),
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair), "quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
@@ -2337,8 +2340,8 @@ class FreqtradeBot(LoggingMixin):
def _update_trade_after_fill(self, trade: Trade, order: Order, send_msg: bool) -> Trade: def _update_trade_after_fill(self, trade: Trade, order: Order, send_msg: bool) -> Trade:
if order.status in constants.NON_OPEN_EXCHANGE_STATES: if order.status in constants.NON_OPEN_EXCHANGE_STATES:
strategy_safe_wrapper(self.strategy.order_filled, default_retval=None)( strategy_safe_wrapper(self.strategy.order_filled, supress_error=True)(
pair=trade.pair, trade=trade, order=order, current_time=datetime.now(timezone.utc) pair=trade.pair, trade=trade, order=order, current_time=datetime.now(UTC)
) )
# If a entry order was closed, force update on stoploss on exchange # If a entry order was closed, force update on stoploss on exchange
if order.ft_order_side == trade.entry_side: if order.ft_order_side == trade.entry_side:
@@ -2365,14 +2368,14 @@ class FreqtradeBot(LoggingMixin):
stake_currency=self.config["stake_currency"], stake_currency=self.config["stake_currency"],
dry_run=self.config["dry_run"], dry_run=self.config["dry_run"],
) )
if self.strategy.use_custom_stoploss: if self.strategy.use_custom_stoploss and trade.is_open:
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
) )
profit = trade.calc_profit_ratio(current_rate) profit = trade.calc_profit_ratio(current_rate)
self.strategy.ft_stoploss_adjust( self.strategy.ft_stoploss_adjust(
current_rate, trade, datetime.now(timezone.utc), profit, 0, after_fill=True current_rate, trade, datetime.now(UTC), profit, 0, after_fill=True
) )
# Updating wallets when order is closed # Updating wallets when order is closed
self.wallets.update() self.wallets.update()
return trade return trade
@@ -2397,7 +2400,7 @@ class FreqtradeBot(LoggingMixin):
def handle_protections(self, pair: str, side: LongShort) -> None: def handle_protections(self, pair: str, side: LongShort) -> None:
# Lock pair for one candle to prevent immediate re-entries # Lock pair for one candle to prevent immediate re-entries
self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason="Auto lock", side=side) self.strategy.lock_pair(pair, datetime.now(UTC), reason="Auto lock", side=side)
prot_trig = self.protections.stop_per_pair(pair, side=side) prot_trig = self.protections.stop_per_pair(pair, side=side)
if prot_trig: if prot_trig:
msg: RPCProtectionMsg = { msg: RPCProtectionMsg = {
+1 -1
View File
@@ -8,4 +8,4 @@ from freqtrade.ft_types.backtest_result_type import (
get_BacktestResultType_default, get_BacktestResultType_default,
) )
from freqtrade.ft_types.plot_annotation_type import AnnotationType from freqtrade.ft_types.plot_annotation_type import AnnotationType
from freqtrade.ft_types.valid_exchanges_type import ValidExchangesType from freqtrade.ft_types.valid_exchanges_type import TradeModeType, ValidExchangesType
+2 -2
View File
@@ -1,8 +1,8 @@
from datetime import datetime from datetime import datetime
from typing import Literal from typing import Literal, Required
from pydantic import TypeAdapter from pydantic import TypeAdapter
from typing_extensions import Required, TypedDict from typing_extensions import TypedDict
class AnnotationType(TypedDict, total=False): class AnnotationType(TypedDict, total=False):
+2 -2
View File
@@ -58,7 +58,7 @@ def setup_logging_pre() -> None:
FT_LOGGING_CONFIG = { FT_LOGGING_CONFIG = {
"version": 1, "version": 1,
# "incremental": True, # "incremental": True,
# "disable_existing_loggers": False, "disable_existing_loggers": False,
"formatters": { "formatters": {
"basic": {"format": "%(message)s"}, "basic": {"format": "%(message)s"},
"standard": { "standard": {
@@ -223,7 +223,7 @@ def setup_logging(config: Config) -> None:
logger.info("Enabling colorized output.") logger.info("Enabling colorized output.")
error_console._color_system = error_console._detect_color_system() error_console._color_system = error_console._detect_color_system()
logging.info("Logfile configured") logger.info("Logfile configured")
# Set verbosity levels # Set verbosity levels
logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG) logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG)
+2 -2
View File
@@ -10,8 +10,8 @@ from typing import Any
# check min. python version # check min. python version
if sys.version_info < (3, 10): # pragma: no cover # noqa: UP036 if sys.version_info < (3, 11): # pragma: no cover # noqa: UP036
sys.exit("Freqtrade requires Python version >= 3.10") sys.exit("Freqtrade requires Python version >= 3.11")
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.commands import Arguments from freqtrade.commands import Arguments
+1
View File
@@ -125,6 +125,7 @@ class LookaheadAnalysis(BaseAnalysis):
backtesting = Backtesting(prepare_data_config, self.exchange) backtesting = Backtesting(prepare_data_config, self.exchange)
self.exchange = backtesting.exchange self.exchange = backtesting.exchange
self.local_config["candle_type_def"] = prepare_data_config["candle_type_def"]
self._fee = backtesting.fee self._fee = backtesting.fee
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
+1 -1
View File
@@ -743,7 +743,7 @@ class Backtesting:
if order and self._get_order_filled(order.ft_price, row): if order and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_date, trade) order.close_bt_order(current_date, trade)
self._run_funding_fees(trade, current_date, force=True) self._run_funding_fees(trade, current_date, force=True)
strategy_safe_wrapper(self.strategy.order_filled, default_retval=None)( strategy_safe_wrapper(self.strategy.order_filled, supress_error=True)(
pair=trade.pair, pair=trade.pair,
trade=trade, # type: ignore[arg-type] trade=trade, # type: ignore[arg-type]
order=order, order=order,
+4 -4
View File
@@ -1,6 +1,6 @@
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import UTC, datetime
from typing import Any from typing import Any
from pandas import DataFrame from pandas import DataFrame
@@ -38,7 +38,7 @@ class BaseAnalysis:
@staticmethod @staticmethod
def dt_to_timestamp(dt: datetime): def dt_to_timestamp(dt: datetime):
timestamp = int(dt.replace(tzinfo=timezone.utc).timestamp()) timestamp = int(dt.replace(tzinfo=UTC).timestamp())
return timestamp return timestamp
def fill_full_varholder(self): def fill_full_varholder(self):
@@ -48,12 +48,12 @@ class BaseAnalysis:
parsed_timerange = TimeRange.parse_timerange(self.local_config["timerange"]) parsed_timerange = TimeRange.parse_timerange(self.local_config["timerange"])
if parsed_timerange.startdt is None: if parsed_timerange.startdt is None:
self.full_varHolder.from_dt = datetime.fromtimestamp(0, tz=timezone.utc) self.full_varHolder.from_dt = datetime.fromtimestamp(0, tz=UTC)
else: else:
self.full_varHolder.from_dt = parsed_timerange.startdt self.full_varHolder.from_dt = parsed_timerange.startdt
if parsed_timerange.stopdt is None: if parsed_timerange.stopdt is None:
self.full_varHolder.to_dt = datetime.now(timezone.utc) self.full_varHolder.to_dt = datetime.now(UTC)
else: else:
self.full_varHolder.to_dt = parsed_timerange.stopdt self.full_varHolder.to_dt = parsed_timerange.stopdt
@@ -6,7 +6,7 @@ and will be sent to the hyperopt worker processes.
import logging import logging
import sys import sys
import warnings import warnings
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -273,7 +273,7 @@ class HyperOptimizer:
Keep this function as optimized as possible! Keep this function as optimized as possible!
""" """
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE) HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
backtest_start_time = datetime.now(timezone.utc) backtest_start_time = datetime.now(UTC)
# Apply parameters # Apply parameters
if HyperoptTools.has_space(self.config, "buy"): if HyperoptTools.has_space(self.config, "buy"):
@@ -330,7 +330,7 @@ class HyperOptimizer:
bt_results = self.backtesting.backtest( bt_results = self.backtesting.backtest(
processed=processed, start_date=self.min_date, end_date=self.max_date processed=processed, start_date=self.min_date, end_date=self.max_date
) )
backtest_end_time = datetime.now(timezone.utc) backtest_end_time = datetime.now(UTC)
bt_results.update( bt_results.update(
{ {
"backtest_start_time": int(backtest_start_time.timestamp()), "backtest_start_time": int(backtest_start_time.timestamp()),
+2 -2
View File
@@ -1,7 +1,7 @@
import logging import logging
from collections.abc import Iterator from collections.abc import Iterator
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -71,7 +71,7 @@ class HyperoptTools:
"strategy_name": strategy_name, "strategy_name": strategy_name,
"params": final_params, "params": final_params,
"ft_stratparam_v": 1, "ft_stratparam_v": 1,
"export_time": datetime.now(timezone.utc), "export_time": datetime.now(UTC),
} }
logger.info(f"Dumping parameters to {filename}") logger.info(f"Dumping parameters to {filename}")
with filename.open("w") as f: with filename.open("w") as f:
@@ -332,8 +332,11 @@ def text_table_add_metrics(strat_results: dict) -> None:
), ),
), ),
( (
"Avg. daily profit %", "Avg. daily profit",
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}", fmt_coin(
(strat_results["profit_total_abs"] / strat_results["backtest_days"]),
strat_results["stake_currency"],
),
), ),
( (
"Avg. stake amount", "Avg. stake amount",
@@ -1,6 +1,6 @@
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from typing import Any, Literal from typing import Any, Literal
import numpy as np import numpy as np
@@ -83,7 +83,6 @@ def _generate_result_line(
""" """
Generate one result dict, with "first_column" as key. Generate one result dict, with "first_column" as key.
""" """
profit_sum = result["profit_ratio"].sum()
# (end-capital - starting capital) / starting capital # (end-capital - starting capital) / starting capital
profit_total = result["profit_abs"].sum() / starting_balance profit_total = result["profit_abs"].sum() / starting_balance
backtest_days = (max_date - min_date).days or 1 backtest_days = (max_date - min_date).days or 1
@@ -108,8 +107,6 @@ def _generate_result_line(
"profit_mean_pct": ( "profit_mean_pct": (
round(result["profit_ratio"].mean() * 100.0, 2) if len(result) > 0 else 0.0 round(result["profit_ratio"].mean() * 100.0, 2) if len(result) > 0 else 0.0
), ),
"profit_sum": profit_sum,
"profit_sum_pct": round(profit_sum * 100.0, 2),
"profit_total_abs": result["profit_abs"].sum(), "profit_total_abs": result["profit_abs"].sum(),
"profit_total": profit_total, "profit_total": profit_total,
"profit_total_pct": round(profit_total * 100.0, 2), "profit_total_pct": round(profit_total * 100.0, 2),
@@ -518,14 +515,16 @@ def generate_strategy_stats(
best_pair = ( best_pair = (
max( max(
[pair for pair in pair_results if pair["key"] != "TOTAL"], key=lambda x: x["profit_sum"] [pair for pair in pair_results if pair["key"] != "TOTAL"],
key=lambda x: x["profit_total_abs"],
) )
if len(pair_results) > 1 if len(pair_results) > 1
else None else None
) )
worst_pair = ( worst_pair = (
min( min(
[pair for pair in pair_results if pair["key"] != "TOTAL"], key=lambda x: x["profit_sum"] [pair for pair in pair_results if pair["key"] != "TOTAL"],
key=lambda x: x["profit_total_abs"],
) )
if len(pair_results) > 1 if len(pair_results) > 1
else None else None
@@ -652,9 +651,9 @@ def generate_strategy_stats(
"max_drawdown_abs": 0.0, "max_drawdown_abs": 0.0,
"max_drawdown_low": 0.0, "max_drawdown_low": 0.0,
"max_drawdown_high": 0.0, "max_drawdown_high": 0.0,
"drawdown_start": datetime(1970, 1, 1, tzinfo=timezone.utc), "drawdown_start": datetime(1970, 1, 1, tzinfo=UTC),
"drawdown_start_ts": 0, "drawdown_start_ts": 0,
"drawdown_end": datetime(1970, 1, 1, tzinfo=timezone.utc), "drawdown_end": datetime(1970, 1, 1, tzinfo=UTC),
"drawdown_end_ts": 0, "drawdown_end_ts": 0,
"csum_min": 0, "csum_min": 0,
"csum_max": 0, "csum_max": 0,
+5 -5
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timezone from datetime import UTC, datetime
from enum import Enum from enum import Enum
from typing import ClassVar, Literal from typing import ClassVar, Literal
@@ -114,7 +114,7 @@ class KeyValueStore:
if kv.value_type == ValueTypesEnum.STRING: if kv.value_type == ValueTypesEnum.STRING:
return kv.string_value return kv.string_value
if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None: if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None:
return kv.datetime_value.replace(tzinfo=timezone.utc) return kv.datetime_value.replace(tzinfo=UTC)
if kv.value_type == ValueTypesEnum.FLOAT: if kv.value_type == ValueTypesEnum.FLOAT:
return kv.float_value return kv.float_value
if kv.value_type == ValueTypesEnum.INT: if kv.value_type == ValueTypesEnum.INT:
@@ -156,7 +156,7 @@ class KeyValueStore:
) )
if kv is None or kv.datetime_value is None: if kv is None or kv.datetime_value is None:
return None return None
return kv.datetime_value.replace(tzinfo=timezone.utc) return kv.datetime_value.replace(tzinfo=UTC)
@staticmethod @staticmethod
def get_float_value(key: KeyStoreKeys) -> float | None: def get_float_value(key: KeyStoreKeys) -> float | None:
@@ -207,5 +207,5 @@ def set_startup_time() -> None:
if t is not None: if t is not None:
KeyValueStore.store_value("bot_start_time", t.open_date_utc) KeyValueStore.store_value("bot_start_time", t.open_date_utc)
else: else:
KeyValueStore.store_value("bot_start_time", datetime.now(timezone.utc)) KeyValueStore.store_value("bot_start_time", datetime.now(UTC))
KeyValueStore.store_value("startup_time", datetime.now(timezone.utc)) KeyValueStore.store_value("startup_time", datetime.now(UTC))
+3 -5
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timezone from datetime import UTC, datetime
from typing import Any, ClassVar from typing import Any, ClassVar
from sqlalchemy import ScalarResult, String, or_, select from sqlalchemy import ScalarResult, String, or_, select
@@ -69,11 +69,9 @@ class PairLock(ModelBase):
"id": self.id, "id": self.id,
"pair": self.pair, "pair": self.pair,
"lock_time": self.lock_time.strftime(DATETIME_PRINT_FORMAT), "lock_time": self.lock_time.strftime(DATETIME_PRINT_FORMAT),
"lock_timestamp": int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), "lock_timestamp": int(self.lock_time.replace(tzinfo=UTC).timestamp() * 1000),
"lock_end_time": self.lock_end_time.strftime(DATETIME_PRINT_FORMAT), "lock_end_time": self.lock_end_time.strftime(DATETIME_PRINT_FORMAT),
"lock_end_timestamp": int( "lock_end_timestamp": int(self.lock_end_time.replace(tzinfo=UTC).timestamp() * 1000),
self.lock_end_time.replace(tzinfo=timezone.utc).timestamp() * 1000
),
"reason": self.reason, "reason": self.reason,
"side": self.side, "side": self.side,
"active": self.active, "active": self.active,
+7 -7
View File
@@ -1,6 +1,6 @@
import logging import logging
from collections.abc import Sequence from collections.abc import Sequence
from datetime import datetime, timezone from datetime import UTC, datetime
from sqlalchemy import select from sqlalchemy import select
@@ -52,7 +52,7 @@ class PairLocks:
""" """
lock = PairLock( lock = PairLock(
pair=pair, pair=pair,
lock_time=now or datetime.now(timezone.utc), lock_time=now or datetime.now(UTC),
lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until), lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until),
reason=reason, reason=reason,
side=side, side=side,
@@ -77,7 +77,7 @@ class PairLocks:
:param side: Side get locks for, can be 'long', 'short', '*' or None :param side: Side get locks for, can be 'long', 'short', '*' or None
""" """
if not now: if not now:
now = datetime.now(timezone.utc) now = datetime.now(UTC)
if PairLocks.use_db: if PairLocks.use_db:
return PairLock.query_pair_locks(pair, now, side).all() return PairLock.query_pair_locks(pair, now, side).all()
@@ -114,7 +114,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc) defaults to datetime.now(timezone.utc)
""" """
if not now: if not now:
now = datetime.now(timezone.utc) now = datetime.now(UTC)
logger.info(f"Releasing all locks for {pair}.") logger.info(f"Releasing all locks for {pair}.")
locks = PairLocks.get_pair_locks(pair, now, side=side) locks = PairLocks.get_pair_locks(pair, now, side=side)
@@ -132,7 +132,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc) defaults to datetime.now(timezone.utc)
""" """
if not now: if not now:
now = datetime.now(timezone.utc) now = datetime.now(UTC)
if PairLocks.use_db: if PairLocks.use_db:
# used in live modes # used in live modes
@@ -161,7 +161,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc) defaults to datetime.now(timezone.utc)
""" """
if not now: if not now:
now = datetime.now(timezone.utc) now = datetime.now(UTC)
return len(PairLocks.get_pair_locks("*", now, side)) > 0 return len(PairLocks.get_pair_locks("*", now, side)) > 0
@@ -173,7 +173,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc) defaults to datetime.now(timezone.utc)
""" """
if not now: if not now:
now = datetime.now(timezone.utc) now = datetime.now(UTC)
return len(PairLocks.get_pair_locks(pair, now, side)) > 0 or PairLocks.is_global_lock( return len(PairLocks.get_pair_locks(pair, now, side)) > 0 or PairLocks.is_global_lock(
now, side now, side
+24 -25
View File
@@ -6,9 +6,9 @@ import logging
from collections import defaultdict from collections import defaultdict
from collections.abc import Sequence from collections.abc import Sequence
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import UTC, datetime
from math import isclose from math import isclose
from typing import Any, ClassVar, Optional, cast from typing import Any, ClassVar, Optional, Self, cast
from sqlalchemy import ( from sqlalchemy import (
Enum, Enum,
@@ -25,7 +25,6 @@ from sqlalchemy import (
select, select,
) )
from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship, validates from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship, validates
from typing_extensions import Self
from freqtrade.constants import ( from freqtrade.constants import (
CANCELED_EXCHANGE_STATES, CANCELED_EXCHANGE_STATES,
@@ -121,14 +120,12 @@ class Order(ModelBase):
@property @property
def order_date_utc(self) -> datetime: def order_date_utc(self) -> datetime:
"""Order-date with UTC timezoneinfo""" """Order-date with UTC timezoneinfo"""
return self.order_date.replace(tzinfo=timezone.utc) return self.order_date.replace(tzinfo=UTC)
@property @property
def order_filled_utc(self) -> datetime | None: def order_filled_utc(self) -> datetime | None:
"""last order-date with UTC timezoneinfo""" """last order-date with UTC timezoneinfo"""
return ( return self.order_filled_date.replace(tzinfo=UTC) if self.order_filled_date else None
self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None
)
@property @property
def safe_amount(self) -> float: def safe_amount(self) -> float:
@@ -229,7 +226,7 @@ class Order(ModelBase):
self.order_filled_date = dt_from_ts( self.order_filled_date = dt_from_ts(
safe_value_fallback(order, "lastTradeTimestamp", default_value=dt_ts()) safe_value_fallback(order, "lastTradeTimestamp", default_value=dt_ts())
) )
self.order_update_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(UTC)
def to_ccxt_object(self, stopPriceName: str = "stopPrice") -> dict[str, Any]: def to_ccxt_object(self, stopPriceName: str = "stopPrice") -> dict[str, Any]:
order: dict[str, Any] = { order: dict[str, Any] = {
@@ -286,7 +283,7 @@ class Order(ModelBase):
self.order_date.strftime(DATETIME_PRINT_FORMAT) if self.order_date else None self.order_date.strftime(DATETIME_PRINT_FORMAT) if self.order_date else None
), ),
"order_timestamp": ( "order_timestamp": (
int(self.order_date.replace(tzinfo=timezone.utc).timestamp() * 1000) int(self.order_date.replace(tzinfo=UTC).timestamp() * 1000)
if self.order_date if self.order_date
else None else None
), ),
@@ -533,7 +530,7 @@ class LocalTrade:
@property @property
def open_date_utc(self): def open_date_utc(self):
return self.open_date.replace(tzinfo=timezone.utc) return self.open_date.replace(tzinfo=UTC)
@property @property
def stoploss_last_update_utc(self): def stoploss_last_update_utc(self):
@@ -543,7 +540,7 @@ class LocalTrade:
@property @property
def close_date_utc(self): def close_date_utc(self):
return self.close_date.replace(tzinfo=timezone.utc) if self.close_date else None return self.close_date.replace(tzinfo=UTC) if self.close_date else None
@property @property
def entry_side(self) -> str: def entry_side(self) -> str:
@@ -1056,7 +1053,7 @@ class LocalTrade:
return zero return zero
open_date = self.open_date.replace(tzinfo=None) open_date = self.open_date.replace(tzinfo=None)
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None) now = (self.close_date or datetime.now(UTC)).replace(tzinfo=None)
sec_per_hour = FtPrecise(3600) sec_per_hour = FtPrecise(3600)
total_seconds = FtPrecise((now - open_date).total_seconds()) total_seconds = FtPrecise((now - open_date).total_seconds())
hours = total_seconds / sec_per_hour or zero hours = total_seconds / sec_per_hour or zero
@@ -1572,12 +1569,12 @@ class LocalTrade:
fee_close=data["fee_close"], fee_close=data["fee_close"],
fee_close_cost=data.get("fee_close_cost"), fee_close_cost=data.get("fee_close_cost"),
fee_close_currency=data.get("fee_close_currency"), fee_close_currency=data.get("fee_close_currency"),
open_date=datetime.fromtimestamp(data["open_timestamp"] // 1000, tz=timezone.utc), open_date=datetime.fromtimestamp(data["open_timestamp"] // 1000, tz=UTC),
open_rate=data["open_rate"], open_rate=data["open_rate"],
open_rate_requested=data.get("open_rate_requested", data["open_rate"]), open_rate_requested=data.get("open_rate_requested", data["open_rate"]),
open_trade_value=data.get("open_trade_value"), open_trade_value=data.get("open_trade_value"),
close_date=( close_date=(
datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc) datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=UTC)
if data["close_timestamp"] if data["close_timestamp"]
else None else None
), ),
@@ -1622,7 +1619,7 @@ class LocalTrade:
if order.get("order_date") if order.get("order_date")
else None, else None,
order_filled_date=( order_filled_date=(
datetime.fromtimestamp(order["order_filled_timestamp"] // 1000, tz=timezone.utc) datetime.fromtimestamp(order["order_filled_timestamp"] // 1000, tz=UTC)
if order["order_filled_timestamp"] if order["order_filled_timestamp"]
else None else None
), ),
@@ -2093,32 +2090,34 @@ class Trade(ModelBase, LocalTrade):
return resp return resp
@staticmethod @staticmethod
def get_best_pair(start_date: datetime | None = None): def get_best_pair(trade_filter: list | None = None):
""" """
Get best pair with closed trade. Get best pair with closed trade.
NOTE: Not supported in Backtesting. NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum) :returns: Tuple containing (pair, profit_sum)
""" """
filters: list = [Trade.is_open.is_(False)] if not trade_filter:
if start_date: trade_filter = []
filters.append(Trade.close_date >= start_date) trade_filter.append(Trade.is_open.is_(False))
pair_rates_query = Trade._generic_performance_query([Trade.pair], filters) pair_rates_query = Trade._generic_performance_query([Trade.pair], trade_filter)
best_pair = Trade.session.execute(pair_rates_query).first() best_pair = Trade.session.execute(pair_rates_query).first()
# returns pair, profit_ratio, abs_profit, count # returns pair, profit_ratio, abs_profit, count
return best_pair return best_pair
@staticmethod @staticmethod
def get_trading_volume(start_date: datetime | None = None) -> float: def get_trading_volume(trade_filter: list | None = None) -> float:
""" """
Get Trade volume based on Orders Get Trade volume based on Orders
NOTE: Not supported in Backtesting. NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum) :returns: Tuple containing (pair, profit_sum)
""" """
filters = [Order.status == "closed"] if not trade_filter:
if start_date: trade_filter = []
filters.append(Order.order_filled_date >= start_date) trade_filter.append(Order.status == "closed")
trading_volume = Trade.session.execute( trading_volume = Trade.session.execute(
select(func.sum(Order.cost).label("volume")).filter(*filters) select(func.sum(Order.cost).label("volume"))
.join(Order._trade_live)
.filter(*trade_filter)
).scalar_one() ).scalar_one()
return trading_volume or 0.0 return trading_volume or 0.0
+2 -2
View File
@@ -1,5 +1,5 @@
import logging import logging
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
import pandas as pd import pandas as pd
@@ -638,7 +638,7 @@ def load_and_plot_trades(config: Config):
exchange = ExchangeResolver.load_exchange(config) exchange = ExchangeResolver.load_exchange(config)
IStrategy.dp = DataProvider(config, exchange) IStrategy.dp = DataProvider(config, exchange)
strategy.ft_bot_start() strategy.ft_bot_start()
strategy_safe_wrapper(strategy.bot_loop_start)(current_time=datetime.now(timezone.utc)) strategy_safe_wrapper(strategy.bot_loop_start)(current_time=datetime.now(UTC))
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count) plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements["timerange"] timerange = plot_elements["timerange"]
trades = plot_elements["trades"] trades = plot_elements["trades"]
+3 -3
View File
@@ -3,7 +3,7 @@ Protection manager class
""" """
import logging import logging
from datetime import datetime, timezone from datetime import UTC, datetime
from typing import Any from typing import Any
from freqtrade.constants import Config, LongShort from freqtrade.constants import Config, LongShort
@@ -49,7 +49,7 @@ class ProtectionManager:
def global_stop(self, now: datetime | None = None, side: LongShort = "long") -> PairLock | None: def global_stop(self, now: datetime | None = None, side: LongShort = "long") -> PairLock | None:
if not now: if not now:
now = datetime.now(timezone.utc) now = datetime.now(UTC)
result = None result = None
for protection_handler in self._protection_handlers: for protection_handler in self._protection_handlers:
if protection_handler.has_global_stop: if protection_handler.has_global_stop:
@@ -65,7 +65,7 @@ class ProtectionManager:
self, pair, now: datetime | None = None, side: LongShort = "long" self, pair, now: datetime | None = None, side: LongShort = "long"
) -> PairLock | None: ) -> PairLock | None:
if not now: if not now:
now = datetime.now(timezone.utc) now = datetime.now(UTC)
result = None result = None
for protection_handler in self._protection_handlers: for protection_handler in self._protection_handlers:
if protection_handler.has_local_stop: if protection_handler.has_local_stop:
+2 -2
View File
@@ -1,7 +1,7 @@
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
from freqtrade.constants import Config, LongShort from freqtrade.constants import Config, LongShort
@@ -127,7 +127,7 @@ class IProtection(LoggingMixin, ABC):
max_date: datetime = max([trade.close_date for trade in trades if trade.close_date]) max_date: datetime = max([trade.close_date for trade in trades if trade.close_date])
# coming from Database, tzinfo is not set. # coming from Database, tzinfo is not set.
if max_date.tzinfo is None: if max_date.tzinfo is None:
max_date = max_date.replace(tzinfo=timezone.utc) max_date = max_date.replace(tzinfo=UTC)
if self._unlock_at is not None: if self._unlock_at is not None:
# unlock_at case with fixed hour of the day # unlock_at case with fixed hour of the day
+1 -1
View File
@@ -54,7 +54,7 @@ class StrategyResolver(IResolver):
strategy.ft_load_params_from_file() strategy.ft_load_params_from_file()
# Set attributes # Set attributes
# Check if we need to override configuration # Check if we need to override configuration
# (Attribute name, default, subkey) # (Attribute name, default, subkey)
attributes = [ attributes = [
("minimal_roi", {"0": 10.0}), ("minimal_roi", {"0": 10.0}),
("timeframe", None), ("timeframe", None),
+4 -4
View File
@@ -1,6 +1,6 @@
import logging import logging
import secrets import secrets
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
import jwt import jwt
@@ -89,15 +89,15 @@ async def validate_ws_token(
def create_token(data: dict, secret_key: str, token_type: str = "access") -> str: # noqa: S107 def create_token(data: dict, secret_key: str, token_type: str = "access") -> str: # noqa: S107
to_encode = data.copy() to_encode = data.copy()
if token_type == "access": # noqa: S105 if token_type == "access": # noqa: S105
expire = datetime.now(timezone.utc) + timedelta(minutes=15) expire = datetime.now(UTC) + timedelta(minutes=15)
elif token_type == "refresh": # noqa: S105 elif token_type == "refresh": # noqa: S105
expire = datetime.now(timezone.utc) + timedelta(days=30) expire = datetime.now(UTC) + timedelta(days=30)
else: else:
raise ValueError() raise ValueError()
to_encode.update( to_encode.update(
{ {
"exp": expire, "exp": expire,
"iat": datetime.now(timezone.utc), "iat": datetime.now(UTC),
"type": token_type, "type": token_type,
} }
) )
+11
View File
@@ -163,11 +163,22 @@ class Profit(BaseModel):
max_drawdown_start_timestamp: int max_drawdown_start_timestamp: int
max_drawdown_end: str max_drawdown_end: str
max_drawdown_end_timestamp: int max_drawdown_end_timestamp: int
current_drawdown: float
current_drawdown_abs: float
current_drawdown_high: float
current_drawdown_start: str
current_drawdown_start_timestamp: int
trading_volume: float | None = None trading_volume: float | None = None
bot_start_timestamp: int bot_start_timestamp: int
bot_start_date: str bot_start_date: str
class ProfitAll(BaseModel):
all: Profit
long: Profit | None = None
short: Profit | None = None
class SellReason(BaseModel): class SellReason(BaseModel):
wins: int wins: int
losses: int losses: int
+21 -1
View File
@@ -43,6 +43,7 @@ from freqtrade.rpc.api_server.api_schemas import (
Ping, Ping,
PlotConfig, PlotConfig,
Profit, Profit,
ProfitAll,
ResultMsg, ResultMsg,
ShowConfig, ShowConfig,
Stats, Stats,
@@ -89,7 +90,8 @@ logger = logging.getLogger(__name__)
# 2.40: Add hyperopt-loss endpoint # 2.40: Add hyperopt-loss endpoint
# 2.41: Add download-data endpoint # 2.41: Add download-data endpoint
# 2.42: Add /pair_history endpoint with live data # 2.42: Add /pair_history endpoint with live data
API_VERSION = 2.42 # 2.43: Add /profit_all endpoint
API_VERSION = 2.43
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()
@@ -148,6 +150,24 @@ def profit(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
return rpc._rpc_trade_statistics(config["stake_currency"], config.get("fiat_display_currency")) return rpc._rpc_trade_statistics(config["stake_currency"], config.get("fiat_display_currency"))
@router.get("/profit_all", response_model=ProfitAll, tags=["info"])
def profit_all(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
response = {
"all": rpc._rpc_trade_statistics(
config["stake_currency"], config.get("fiat_display_currency")
),
}
if config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
response["long"] = rpc._rpc_trade_statistics(
config["stake_currency"], config.get("fiat_display_currency"), direction="long"
)
response["short"] = rpc._rpc_trade_statistics(
config["stake_currency"], config.get("fiat_display_currency"), direction="short"
)
return response
@router.get("/stats", response_model=Stats, tags=["info"]) @router.get("/stats", response_model=Stats, tags=["info"])
def stats(rpc: RPC = Depends(get_rpc)): def stats(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_stats() return rpc._rpc_stats()
+2 -2
View File
@@ -1,7 +1,7 @@
from typing import Any, Literal from typing import Any, Literal, NotRequired
from uuid import uuid4 from uuid import uuid4
from typing_extensions import NotRequired, TypedDict from typing_extensions import TypedDict
from freqtrade.exchange.exchange import Exchange from freqtrade.exchange.exchange import Exchange
+2 -2
View File
@@ -102,7 +102,7 @@ class WebSocketChannel:
self._send_times.append(total_time) self._send_times.append(total_time)
self._calc_send_limit() self._calc_send_limit()
except asyncio.TimeoutError: except TimeoutError:
logger.info(f"Connection for {self} timed out, disconnecting") logger.info(f"Connection for {self} timed out, disconnecting")
raise raise
@@ -201,8 +201,8 @@ class WebSocketChannel:
try: try:
await task await task
except ( except (
TimeoutError,
asyncio.CancelledError, asyncio.CancelledError,
asyncio.TimeoutError,
WebSocketDisconnect, WebSocketDisconnect,
ConnectionClosed, ConnectionClosed,
RuntimeError, RuntimeError,
+1 -1
View File
@@ -266,7 +266,7 @@ class ExternalMessageConsumer:
except Exception as e: except Exception as e:
logger.exception(f"Error handling producer message: {e}") logger.exception(f"Error handling producer message: {e}")
except (asyncio.TimeoutError, websockets.exceptions.ConnectionClosed): except (TimeoutError, websockets.exceptions.ConnectionClosed):
# We haven't received data yet. Check the connection and continue. # We haven't received data yet. Check the connection and continue.
try: try:
# ping # ping
+82 -24
View File
@@ -5,7 +5,7 @@ This module contains class to define a RPC communications
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from collections.abc import Generator, Sequence from collections.abc import Generator, Sequence
from datetime import date, datetime, timedelta, timezone from datetime import UTC, date, datetime, timedelta
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import psutil import psutil
@@ -34,7 +34,7 @@ from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_msec
from freqtrade.exchange.exchange_utils import price_to_precision from freqtrade.exchange.exchange_utils import price_to_precision
from freqtrade.ft_types import AnnotationType from freqtrade.ft_types import AnnotationType
from freqtrade.loggers import bufferHandler from freqtrade.loggers import bufferHandler
from freqtrade.persistence import CustomDataWrapper, KeyValueStore, PairLocks, Trade from freqtrade.persistence import CustomDataWrapper, KeyValueStore, Order, PairLocks, Trade
from freqtrade.persistence.models import PairLock, custom_data_rpc_wrapper from freqtrade.persistence.models import PairLock, custom_data_rpc_wrapper
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@@ -375,7 +375,7 @@ class RPC:
""" """
:param timeunit: Valid entries are 'days', 'weeks', 'months' :param timeunit: Valid entries are 'days', 'weeks', 'months'
""" """
start_date = datetime.now(timezone.utc).date() start_date = datetime.now(UTC).date()
if timeunit == "weeks": if timeunit == "weeks":
# weekly # weekly
start_date = start_date - timedelta(days=start_date.weekday()) # Monday start_date = start_date - timedelta(days=start_date.weekday()) # Monday
@@ -502,20 +502,13 @@ class RPC:
durations = {"wins": wins_dur, "draws": draws_dur, "losses": losses_dur} durations = {"wins": wins_dur, "draws": draws_dur, "losses": losses_dur}
return {"exit_reasons": exit_reasons, "durations": durations} return {"exit_reasons": exit_reasons, "durations": durations}
def _rpc_trade_statistics( def _collect_trade_statistics_data(
self, stake_currency: str, fiat_display_currency: str, start_date: datetime | None = None self,
trades: Sequence["Trade"],
stake_currency: str,
fiat_display_currency: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Returns cumulative profit statistics""" """Iterate trades, calculate various statistics, and return intermediate results."""
start_date = datetime.fromtimestamp(0) if start_date is None else start_date
trade_filter = (
Trade.is_open.is_(False) & (Trade.close_date >= start_date)
) | Trade.is_open.is_(True)
trades: Sequence[Trade] = Trade.session.scalars(
Trade.get_trades_query(trade_filter, include_orders=False).order_by(Trade.id)
).all()
profit_all_coin = [] profit_all_coin = []
profit_all_ratio = [] profit_all_ratio = []
profit_closed_coin = [] profit_closed_coin = []
@@ -544,7 +537,7 @@ class RPC:
losing_trades += 1 losing_trades += 1
losing_profit += profit_abs losing_profit += profit_abs
else: else:
# Get current rate # Get current rate for open trades
if len(trade.select_filled_orders(trade.entry_side)) == 0: if len(trade.select_filled_orders(trade.entry_side)) == 0:
# Skip trades with no filled orders # Skip trades with no filled orders
continue continue
@@ -558,17 +551,74 @@ class RPC:
profit_abs = nan profit_abs = nan
else: else:
_profit = trade.calculate_profit(trade.close_rate or current_rate) _profit = trade.calculate_profit(trade.close_rate or current_rate)
profit_ratio = _profit.profit_ratio profit_ratio = _profit.profit_ratio
profit_abs = _profit.total_profit profit_abs = _profit.total_profit
profit_all_coin.append(profit_abs) profit_all_coin.append(profit_abs)
profit_all_ratio.append(profit_ratio) profit_all_ratio.append(profit_ratio)
return {
"profit_all_coin": profit_all_coin,
"profit_all_ratio": profit_all_ratio,
"profit_closed_coin": profit_closed_coin,
"profit_closed_ratio": profit_closed_ratio,
"durations": durations,
"winning_trades": winning_trades,
"losing_trades": losing_trades,
"winning_profit": winning_profit,
"losing_profit": losing_profit,
}
def _rpc_trade_statistics(
self,
stake_currency: str,
fiat_display_currency: str,
start_date: datetime | None = None,
direction: str | None = None,
) -> dict[str, Any]:
"""
Returns cumulative profit statistics, with optional direction filter (long/short)
"""
start_date = datetime.fromtimestamp(0) if start_date is None else start_date
trade_filter = (
Trade.is_open.is_(False) & (Trade.close_date >= start_date)
) | Trade.is_open.is_(True)
if direction == "long":
dir_filter = Trade.is_short.is_(False)
trade_filter = trade_filter & dir_filter
elif direction == "short":
dir_filter = Trade.is_short.is_(True)
trade_filter = trade_filter & dir_filter
trades: Sequence[Trade] = Trade.session.scalars(
Trade.get_trades_query(trade_filter, include_orders=False).order_by(Trade.id)
).all()
stats = self._collect_trade_statistics_data(trades, stake_currency, fiat_display_currency)
profit_all_coin = stats["profit_all_coin"]
profit_all_ratio = stats["profit_all_ratio"]
profit_closed_coin = stats["profit_closed_coin"]
profit_closed_ratio = stats["profit_closed_ratio"]
durations = stats["durations"]
winning_trades = stats["winning_trades"]
losing_trades = stats["losing_trades"]
winning_profit = stats["winning_profit"]
losing_profit = stats["losing_profit"]
closed_trade_count = len([t for t in trades if not t.is_open]) closed_trade_count = len([t for t in trades if not t.is_open])
best_pair = Trade.get_best_pair(start_date) best_pair_filters = [Trade.close_date > start_date]
trading_volume = Trade.get_trading_volume(start_date) trading_volume_filters = [Order.order_filled_date >= start_date]
if direction:
best_pair_filters.append(dir_filter)
trading_volume_filters.append(dir_filter)
best_pair = Trade.get_best_pair(best_pair_filters)
trading_volume = Trade.get_trading_volume(trading_volume_filters)
# Prepare data to display # Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8) profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
@@ -681,6 +731,11 @@ class RPC:
"max_drawdown_end_timestamp": dt_ts_def(drawdown.low_date), "max_drawdown_end_timestamp": dt_ts_def(drawdown.low_date),
"drawdown_high": drawdown.high_value, "drawdown_high": drawdown.high_value,
"drawdown_low": drawdown.low_value, "drawdown_low": drawdown.low_value,
"current_drawdown": drawdown.current_relative_account_drawdown,
"current_drawdown_abs": drawdown.current_drawdown_abs,
"current_drawdown_high": drawdown.current_high_value,
"current_drawdown_start": format_date(drawdown.current_high_date),
"current_drawdown_start_timestamp": dt_ts_def(drawdown.current_high_date),
"trading_volume": trading_volume, "trading_volume": trading_volume,
"bot_start_timestamp": dt_ts_def(bot_start, 0), "bot_start_timestamp": dt_ts_def(bot_start, 0),
"bot_start_date": format_date(bot_start), "bot_start_date": format_date(bot_start),
@@ -1094,7 +1149,7 @@ class RPC:
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
if not trade: if not trade:
logger.warning("delete trade: Invalid argument received") logger.warning("delete trade: Invalid argument received")
raise RPCException("invalid argument") raise RPCException(f"Trade with id '{trade_id}' not found.")
# Try cancelling regular order if that exists # Try cancelling regular order if that exists
for open_order in trade.open_orders: for open_order in trade.open_orders:
@@ -1115,13 +1170,16 @@ class RPC:
c_count += 1 c_count += 1
except ExchangeError: except ExchangeError:
pass pass
trade_pair = trade.pair
trade.delete() trade.delete()
self._freqtrade.wallets.update() self._freqtrade.wallets.update()
return { return {
"result": "success", "result": "success",
"trade_id": trade_id, "trade_id": trade_id,
"result_msg": f"Deleted trade {trade_id}. Closed {c_count} open orders.", "result_msg": (
f"Deleted trade #{trade_id} for pair {trade_pair}. "
f"Closed {c_count} open orders."
),
"cancel_order_count": c_count, "cancel_order_count": c_count,
} }
@@ -1259,7 +1317,7 @@ class RPC:
for lock in locks: for lock in locks:
lock.active = False lock.active = False
lock.lock_end_time = datetime.now(timezone.utc) lock.lock_end_time = datetime.now(UTC)
Trade.commit() Trade.commit()
+2 -2
View File
@@ -56,7 +56,8 @@ class __RPCEntryExitMsgBase(RPCSendMsgBase):
quote_currency: str quote_currency: str
leverage: float | None leverage: float | None
direction: str direction: str
limit: float limit: float # Deprecated, use order_rate instead
order_rate: float
open_rate: float open_rate: float
order_type: str order_type: str
stake_amount: float stake_amount: float
@@ -87,7 +88,6 @@ class RPCExitMsg(__RPCEntryExitMsgBase):
exit_reason: str | None exit_reason: str | None
close_date: datetime close_date: datetime
# current_rate: float | None # current_rate: float | None
order_rate: float | None
final_profit_ratio: float | None final_profit_ratio: float | None
is_final_exit: bool is_final_exit: bool
+166 -71
View File
@@ -191,8 +191,8 @@ class Telegram(RPCHandler):
r"/mix_tags", r"/mix_tags",
r"/daily$", r"/daily$",
r"/daily \d+$", r"/daily \d+$",
r"/profit$", r"/profit([_ ]long|[_ ]short)?$",
r"/profit \d+", r"/profit([_ ]long|[_ ]short)? \d+$",
r"/stats$", r"/stats$",
r"/count$", r"/count$",
r"/locks$", r"/locks$",
@@ -305,13 +305,17 @@ class Telegram(RPCHandler):
CommandHandler("order", self._order), CommandHandler("order", self._order),
CommandHandler("list_custom_data", self._list_custom_data), CommandHandler("list_custom_data", self._list_custom_data),
CommandHandler("tg_info", self._tg_info), CommandHandler("tg_info", self._tg_info),
CommandHandler("profit_long", self._profit_long),
CommandHandler("profit_short", self._profit_short),
] ]
callbacks = [ callbacks = [
CallbackQueryHandler(self._status_table, pattern="update_status_table"), CallbackQueryHandler(self._status_table, pattern="update_status_table"),
CallbackQueryHandler(self._daily, pattern="update_daily"), CallbackQueryHandler(self._daily, pattern="update_daily"),
CallbackQueryHandler(self._weekly, pattern="update_weekly"), CallbackQueryHandler(self._weekly, pattern="update_weekly"),
CallbackQueryHandler(self._monthly, pattern="update_monthly"), CallbackQueryHandler(self._monthly, pattern="update_monthly"),
CallbackQueryHandler(self._profit, pattern="update_profit"), CallbackQueryHandler(self._profit_long, pattern="update_profit_long"),
CallbackQueryHandler(self._profit_short, pattern="update_profit_short"),
CallbackQueryHandler(self._profit, pattern=r"update_profit$"),
CallbackQueryHandler(self._balance, pattern="update_balance"), CallbackQueryHandler(self._balance, pattern="update_balance"),
CallbackQueryHandler(self._performance, pattern="update_performance"), CallbackQueryHandler(self._performance, pattern="update_performance"),
CallbackQueryHandler( CallbackQueryHandler(
@@ -995,29 +999,25 @@ class Telegram(RPCHandler):
""" """
await self._timeunit_stats(update, context, "months") await self._timeunit_stats(update, context, "months")
@authorized_only def _format_profit_message(
async def _profit(self, update: Update, context: CallbackContext) -> None: self,
stats: dict,
stake_cur: str,
fiat_disp_cur: str,
timescale: int | None = None,
direction: str | None = None,
) -> str:
""" """
Handler for /profit. Format profit statistics message for telegram.
Returns a cumulative profit statistics.
:param bot: telegram bot :param stats: Trade statistics dictionary
:param update: message update :param stake_cur: Stake currency
:return: None :param fiat_disp_cur: Fiat display currency
:param timescale: Optional timescale filter
:param direction: Optional direction filter ('long', 'short', or None for all)
:return: Formatted markdown message
""" """
stake_cur = self._config["stake_currency"] # Extract common variables
fiat_disp_cur = self._config.get("fiat_display_currency", "")
start_date = datetime.fromtimestamp(0)
timescale = None
try:
if context.args:
timescale = int(context.args[0]) - 1
today_start = datetime.combine(date.today(), datetime.min.time())
start_date = today_start - timedelta(days=timescale)
except (TypeError, ValueError, IndexError):
pass
stats = self._rpc._rpc_trade_statistics(stake_cur, fiat_disp_cur, start_date)
profit_closed_coin = stats["profit_closed_coin"] profit_closed_coin = stats["profit_closed_coin"]
profit_closed_ratio_mean = stats["profit_closed_ratio_mean"] profit_closed_ratio_mean = stats["profit_closed_ratio_mean"]
profit_closed_percent = stats["profit_closed_percent"] profit_closed_percent = stats["profit_closed_percent"]
@@ -1037,62 +1037,153 @@ class Telegram(RPCHandler):
expectancy = stats["expectancy"] expectancy = stats["expectancy"]
expectancy_ratio = stats["expectancy_ratio"] expectancy_ratio = stats["expectancy_ratio"]
# Direction-specific labels
direction_label = f" {direction}" if direction else ""
no_trades_msg = (
f"No{direction_label} trades yet.\n*Bot started:* `{stats['bot_start_date']}`"
)
no_closed_msg = f"`No closed{direction_label} trade` \n"
closed_roi_label = f"*ROI:* Closed{direction_label} trades"
all_roi_label = f"*ROI:* All{direction_label} trades"
if stats["trade_count"] == 0: if stats["trade_count"] == 0:
markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`" return no_trades_msg
# Build message
if stats["closed_trade_count"] > 0:
fiat_closed_trades = (
f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
)
markdown_msg = (
f"{closed_roi_label}\n"
f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} "
f"({profit_closed_ratio_mean:.2%}) "
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_closed_trades}"
)
else: else:
# Message to display markdown_msg = no_closed_msg
if stats["closed_trade_count"] > 0:
fiat_closed_trades = ( fiat_all_trades = (
f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else "" f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
) )
markdown_msg = ( markdown_msg += (
"*ROI:* Closed trades\n" f"{all_roi_label}\n"
f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} " f"∙ `{fmt_coin(profit_all_coin, stake_cur)} "
f"({profit_closed_ratio_mean:.2%}) " f"({profit_all_ratio_mean:.2%}) "
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_closed_trades}" f"{fiat_all_trades}"
) f"*Total Trade Count:* `{trade_count}`\n"
else: f"*Bot started:* `{stats['bot_start_date']}`\n"
markdown_msg = "`No closed trade` \n" f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
fiat_all_trades = ( f"`{first_trade_date}`\n"
f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else "" f"*Latest Trade opened:* `{latest_trade_date}`\n"
) f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`\n"
f"*Winrate:* `{winrate:.2%}`\n"
f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`"
)
if stats["closed_trade_count"] > 0:
markdown_msg += ( markdown_msg += (
f"*ROI:* All trades\n" f"\n*Avg. Duration:* `{avg_duration}`\n"
f"∙ `{fmt_coin(profit_all_coin, stake_cur)} " f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} "
f"({profit_all_ratio_mean:.2%}) " f"({best_pair_profit_ratio:.2%})`\n"
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n"
f"{fiat_all_trades}" f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
f"*Total Trade Count:* `{trade_count}`\n" f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"*Bot started:* `{stats['bot_start_date']}`\n" f"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n"
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* " f" from `{stats['max_drawdown_start']} "
f"`{first_trade_date}`\n" f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n"
f"*Latest Trade opened:* `{latest_trade_date}`\n" f" to `{stats['max_drawdown_end']} "
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`\n" f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n"
f"*Winrate:* `{winrate:.2%}`\n" f"*Current Drawdown:* `{stats['current_drawdown']:.2%} "
f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`" f"({fmt_coin(stats['current_drawdown_abs'], stake_cur)})`\n"
f" from `{stats['current_drawdown_start']} "
f"({fmt_coin(stats['current_drawdown_high'], stake_cur)})`\n"
) )
if stats["closed_trade_count"] > 0:
markdown_msg += ( return markdown_msg
f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} " async def _profit_handler(
f"({best_pair_profit_ratio:.2%})`\n" self,
f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n" update: Update,
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n" context: CallbackContext,
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} " direction: str | None = None,
f"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n" ) -> None:
f" from `{stats['max_drawdown_start']} " """
f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n" Common handler for profit commands.
f" to `{stats['max_drawdown_end']} "
f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n" :param update: Telegram update
) :param context: Callback context
:param direction: Trade direction filter ('long', 'short', or None)
:param callback_path: Callback path for message updates
"""
stake_cur = self._config["stake_currency"]
fiat_disp_cur = self._config.get("fiat_display_currency", "")
start_date = datetime.fromtimestamp(0)
timescale = None
try:
if context.args:
if not direction:
arg = context.args[0].lower()
if arg in ("short", "long"):
direction = arg
context.args.pop(0) # Remove direction from args
timescale = int(context.args[0]) - 1
today_start = datetime.combine(date.today(), datetime.min.time())
start_date = today_start - timedelta(days=timescale)
except (TypeError, ValueError, IndexError):
pass
# Get stats with optional direction filter
stats_kwargs = {
"stake_currency": stake_cur,
"fiat_display_currency": fiat_disp_cur,
"start_date": start_date,
}
if direction:
stats_kwargs["direction"] = direction
stats = self._rpc._rpc_trade_statistics(**stats_kwargs)
markdown_msg = self._format_profit_message(
stats, stake_cur, fiat_disp_cur, timescale, direction
)
await self._send_msg( await self._send_msg(
markdown_msg, markdown_msg,
reload_able=True, reload_able=True,
callback_path="update_profit", callback_path="update_profit" if not direction else f"update_profit_{direction}",
query=update.callback_query, query=update.callback_query,
) )
@authorized_only
async def _profit(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /profit.
Returns a cumulative profit statistics.
:param bot: telegram bot
:param update: message update
:return: None
"""
await self._profit_handler(update, context)
@authorized_only
async def _profit_long(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /profit_long.
Returns cumulative profit statistics for long trades.
"""
await self._profit_handler(update, context, direction="long")
@authorized_only
async def _profit_short(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /profit_short.
Returns cumulative profit statistics for short trades.
"""
await self._profit_handler(update, context, direction="short")
@authorized_only @authorized_only
async def _stats(self, update: Update, context: CallbackContext) -> None: async def _stats(self, update: Update, context: CallbackContext) -> None:
""" """
@@ -1484,7 +1575,7 @@ class Telegram(RPCHandler):
trade_id = int(context.args[0]) trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id) msg = self._rpc._rpc_delete(trade_id)
await self._send_msg( await self._send_msg(
f"`{msg['result_msg']}`\n" f"{msg['result_msg']}\n"
"Please make sure to take care of this asset on the exchange manually." "Please make sure to take care of this asset on the exchange manually."
) )
@@ -1865,6 +1956,10 @@ class Telegram(RPCHandler):
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, " "*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
"over the last n days`\n" "over the last n days`\n"
"*/profit_long [<n>]:* `Lists cumulative profit from all finished long trades, "
"over the last n days`\n"
"*/profit_short [<n>]:* `Lists cumulative profit from all finished short trades, "
"over the last n days`\n"
"*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n"
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" "*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
"*/weekly <n>:* `Shows statistics per week, over the last n weeks`\n" "*/weekly <n>:* `Shows statistics per week, over the last n weeks`\n"
+3 -3
View File
@@ -5,7 +5,7 @@ This module defines the interface to apply for strategies
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from math import isinf, isnan from math import isinf, isnan
from pandas import DataFrame from pandas import DataFrame
@@ -1149,7 +1149,7 @@ class IStrategy(ABC, HyperStrategyMixin):
manually from within the strategy, to allow an easy way to unlock pairs. manually from within the strategy, to allow an easy way to unlock pairs.
:param pair: Unlock pair to allow trading again :param pair: Unlock pair to allow trading again
""" """
PairLocks.unlock_pair(pair, datetime.now(timezone.utc)) PairLocks.unlock_pair(pair, datetime.now(UTC))
def unlock_reason(self, reason: str) -> None: def unlock_reason(self, reason: str) -> None:
""" """
@@ -1158,7 +1158,7 @@ class IStrategy(ABC, HyperStrategyMixin):
manually from within the strategy, to allow an easy way to unlock pairs. manually from within the strategy, to allow an easy way to unlock pairs.
:param reason: Unlock pairs to allow trading again :param reason: Unlock pairs to allow trading again
""" """
PairLocks.unlock_reason(reason, datetime.now(timezone.utc)) PairLocks.unlock_reason(reason, datetime.now(UTC))
def is_pair_locked( def is_pair_locked(
self, pair: str, *, candle_date: datetime | None = None, side: str = "*" self, pair: str, *, candle_date: datetime | None = None, side: str = "*"
+29 -4
View File
@@ -39,6 +39,17 @@ class StrategyUpdater:
"sell": "exit", "sell": "exit",
} }
# Update function names.
# example: `np.NaN` was removed in the NumPy 2.0 release. Use `np.nan` instead.
module_replacements = {
"numpy": {
"aliases": set(),
"replacements": [
("NaN", "nan"),
],
}
}
# create a dictionary that maps the old column names to the new ones # create a dictionary that maps the old column names to the new ones
rename_dict = {"buy": "enter_long", "sell": "exit_long", "buy_tag": "enter_tag"} rename_dict = {"buy": "enter_long", "sell": "exit_long", "buy_tag": "enter_tag"}
@@ -153,16 +164,24 @@ class NameUpdater(ast_comments.NodeTransformer):
def visit_Name(self, node): def visit_Name(self, node):
# if the name is in the mapping, update it # if the name is in the mapping, update it
node.id = self.check_dict(StrategyUpdater.name_mapping, node.id) node.id = self.check_dict(StrategyUpdater.name_mapping, node.id)
for mod, info in StrategyUpdater.module_replacements.items():
for old_attr, new_attr in info["replacements"]:
if node.id == old_attr:
node.id = new_attr
return node return node
def visit_Import(self, node): def visit_Import(self, node):
# do not update the names in import statements for alias in node.names:
if alias.name in StrategyUpdater.module_replacements:
as_name = alias.asname or alias.name
StrategyUpdater.module_replacements[alias.name]["aliases"].add(as_name)
return node return node
def visit_ImportFrom(self, node): def visit_ImportFrom(self, node):
# if hasattr(node, "module"): if node.module in StrategyUpdater.module_replacements:
# if node.module == "freqtrade.strategy.hyper": mod = node.module
# node.module = "freqtrade.strategy" StrategyUpdater.module_replacements[node.module]["aliases"].add(mod)
return node return node
def visit_If(self, node: ast_comments.If): def visit_If(self, node: ast_comments.If):
@@ -182,6 +201,12 @@ class NameUpdater(ast_comments.NodeTransformer):
and node.attr == "nr_of_successful_buys" and node.attr == "nr_of_successful_buys"
): ):
node.attr = "nr_of_successful_entries" node.attr = "nr_of_successful_entries"
if isinstance(node.value, ast_comments.Name):
for mod, info in StrategyUpdater.module_replacements.items():
if node.value.id in info["aliases"]:
for old_attr, new_attr in info["replacements"]:
if node.attr == old_attr:
node.attr = new_attr
return node return node
def visit_ClassDef(self, node): def visit_ClassDef(self, node):
+4 -4
View File
@@ -1,5 +1,5 @@
import re import re
from datetime import datetime, timezone from datetime import UTC, datetime
from time import time from time import time
import humanize import humanize
@@ -9,7 +9,7 @@ from freqtrade.constants import DATETIME_PRINT_FORMAT
def dt_now() -> datetime: def dt_now() -> datetime:
"""Return the current datetime in UTC.""" """Return the current datetime in UTC."""
return datetime.now(timezone.utc) return datetime.now(UTC)
def dt_utc( def dt_utc(
@@ -22,7 +22,7 @@ def dt_utc(
microsecond: int = 0, microsecond: int = 0,
) -> datetime: ) -> datetime:
"""Return a datetime in UTC.""" """Return a datetime in UTC."""
return datetime(year, month, day, hour, minute, second, microsecond, tzinfo=timezone.utc) return datetime(year, month, day, hour, minute, second, microsecond, tzinfo=UTC)
def dt_ts(dt: datetime | None = None) -> int: def dt_ts(dt: datetime | None = None) -> int:
@@ -68,7 +68,7 @@ def dt_from_ts(timestamp: float) -> datetime:
if timestamp > 1e10: if timestamp > 1e10:
# Timezone in ms - convert to seconds # Timezone in ms - convert to seconds
timestamp /= 1000 timestamp /= 1000
return datetime.fromtimestamp(timestamp, tz=timezone.utc) return datetime.fromtimestamp(timestamp, tz=UTC)
def shorten_date(_date: str) -> str: def shorten_date(_date: str) -> str:
+1 -1
View File
@@ -9,4 +9,4 @@ def get_dry_run_wallet(config: Config) -> int | float:
if isinstance(_start_cap := config["dry_run_wallet"], float | int): if isinstance(_start_cap := config["dry_run_wallet"], float | int):
return _start_cap return _start_cap
else: else:
return _start_cap.get("stake_currency") return _start_cap.get(config["stake_currency"], 0.0)
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timezone from datetime import UTC, datetime
from cachetools import TTLCache from cachetools import TTLCache
@@ -11,7 +11,7 @@ class PeriodicCache(TTLCache):
def __init__(self, maxsize, ttl, getsizeof=None): def __init__(self, maxsize, ttl, getsizeof=None):
def local_timer(): def local_timer():
ts = datetime.now(timezone.utc).timestamp() ts = datetime.now(UTC).timestamp()
offset = ts % ttl offset = ts % ttl
return ts - offset return ts - offset
+1 -1
View File
@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2025.6" __version__ = "2025.7"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path
+1 -2
View File
@@ -13,14 +13,13 @@ authors = [
description = "Freqtrade - Client scripts" description = "Freqtrade - Client scripts"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.11"
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.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
+1 -1
View File
@@ -1,3 +1,3 @@
# Requirements for freqtrade client library # Requirements for freqtrade client library
requests==2.32.4 requests==2.32.4
python-rapidjson==1.20 python-rapidjson==1.21
+2 -2
View File
@@ -13,13 +13,12 @@ authors = [
description = "Freqtrade - Crypto Trading Bot" description = "Freqtrade - Crypto Trading Bot"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.11"
license = {text = "GPLv3"} license = {text = "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.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
@@ -111,6 +110,7 @@ develop = [
"pytest-xdist", "pytest-xdist",
"pytest", "pytest",
"ruff", "ruff",
"scipy-stubs",
"time-machine", "time-machine",
"types-cachetools", "types-cachetools",
"types-filelock", "types-filelock",
+7 -6
View File
@@ -6,16 +6,16 @@
-r requirements-freqai-rl.txt -r requirements-freqai-rl.txt
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
ruff==0.12.1 ruff==0.12.5
mypy==1.16.1 mypy==1.17.0
pre-commit==4.2.0 pre-commit==4.2.0
pytest==8.4.1 pytest==8.4.1
pytest-asyncio==1.0.0 pytest-asyncio==1.1.0
pytest-cov==6.2.1 pytest-cov==6.2.1
pytest-mock==3.14.1 pytest-mock==3.14.1
pytest-random-order==1.2.0 pytest-random-order==1.2.0
pytest-timeout==2.4.0 pytest-timeout==2.4.0
pytest-xdist==3.7.0 pytest-xdist==3.8.0
isort==6.0.1 isort==6.0.1
# For datetime mocking # For datetime mocking
time-machine==2.16.0 time-machine==2.16.0
@@ -24,8 +24,9 @@ time-machine==2.16.0
nbconvert==7.16.6 nbconvert==7.16.6
# mypy types # mypy types
types-cachetools==6.0.0.20250525 scipy-stubs==1.16.0.2 # keep in sync with `scipy` in `requirements-hyperopt.txt`
types-cachetools==6.1.0.20250717
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.32.4.20250611 types-requests==2.32.4.20250611
types-tabulate==0.9.0.20241207 types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20250516 types-python-dateutil==2.9.0.20250708
+1 -1
View File
@@ -5,7 +5,7 @@
torch==2.7.1; sys_platform != 'darwin' or platform_machine != 'x86_64' torch==2.7.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==0.29.1 gymnasium==0.29.1
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos # SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
stable_baselines3==2.6.0; sys_platform != 'darwin' or platform_machine != 'x86_64' stable_baselines3==2.7.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
sb3_contrib>=2.2.1 sb3_contrib>=2.2.1
# Progress bar for stable-baselines3 and sb3-contrib # Progress bar for stable-baselines3 and sb3-contrib
tqdm==4.67.1 tqdm==4.67.1
+2 -2
View File
@@ -3,10 +3,10 @@
-r requirements-plot.txt -r requirements-plot.txt
# Required for freqai # Required for freqai
scikit-learn==1.7.0 scikit-learn==1.7.1
joblib==1.5.1 joblib==1.5.1
catboost==1.2.8; 'arm' not in platform_machine catboost==1.2.8; 'arm' not in platform_machine
lightgbm==4.6.0 lightgbm==4.6.0
xgboost==3.0.2 xgboost==3.0.2
tensorboard==2.19.0 tensorboard==2.20.0
datasieve==0.1.9 datasieve==0.1.9
+3 -3
View File
@@ -2,8 +2,8 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.15.3 scipy==1.16.1
scikit-learn==1.7.0 scikit-learn==1.7.1
filelock==3.18.0 filelock==3.18.0
optuna==4.4.0 optuna==4.4.0
cmaes==0.11.1 cmaes==0.12.0
+14 -14
View File
@@ -1,42 +1,42 @@
numpy==2.2.6 numpy==2.3.2
pandas==2.3.0 pandas==2.3.1
bottleneck==1.5.0 bottleneck==1.5.0
numexpr==2.11.0 numexpr==2.11.0
# Indicator libraries # Indicator libraries
ft-pandas-ta==0.3.15 ft-pandas-ta==0.3.15
ta-lib==0.5.5 ta-lib==0.5.5
technical==1.5.1 technical==1.5.2
ccxt==4.4.91 ccxt==4.4.96
cryptography==45.0.4 cryptography==45.0.5
aiohttp==3.12.13 aiohttp==3.12.14
SQLAlchemy==2.0.41 SQLAlchemy==2.0.41
python-telegram-bot==22.2 python-telegram-bot==22.3
# can't be hard-pinned due to telegram-bot pinning httpx with ~ # can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1 httpx>=0.24.1
humanize==4.12.3 humanize==4.12.3
cachetools==6.1.0 cachetools==6.1.0
requests==2.32.4 requests==2.32.4
urllib3==2.5.0 urllib3==2.5.0
certifi==2025.6.15 certifi==2025.7.14
jsonschema==4.24.0 jsonschema==4.25.0
tabulate==0.9.0 tabulate==0.9.0
pycoingecko==3.2.0 pycoingecko==3.2.0
jinja2==3.1.6 jinja2==3.1.6
joblib==1.5.1 joblib==1.5.1
rich==14.0.0 rich==14.1.0
pyarrow==20.0.0; platform_machine != 'armv7l' pyarrow==21.0.0; platform_machine != 'armv7l'
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.20 python-rapidjson==1.21
# Properly format api responses # Properly format api responses
orjson==3.10.18 orjson==3.11.1
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.115.14 fastapi==0.116.1
pydantic==2.11.7 pydantic==2.11.7
uvicorn==0.35.0 uvicorn==0.35.0
pyjwt==2.10.1 pyjwt==2.10.1
+2 -2
View File
@@ -234,7 +234,7 @@ async def create_client(
await protocol.on_message(ws, name, message) await protocol.on_message(ws, name, message)
except (asyncio.TimeoutError, websockets.exceptions.WebSocketException): except (TimeoutError, websockets.exceptions.WebSocketException):
# Try pinging # Try pinging
try: try:
pong = await ws.ping() pong = await ws.ping()
@@ -244,7 +244,7 @@ async def create_client(
continue continue
except asyncio.TimeoutError: except TimeoutError:
logger.error(f"Ping timed out, retrying in {sleep_time}s") logger.error(f"Ping timed out, retrying in {sleep_time}s")
await asyncio.sleep(sleep_time) await asyncio.sleep(sleep_time)
+3 -6
View File
@@ -153,16 +153,13 @@ function Find-PythonExecutable {
"python3.13", "python3.13",
"python3.12", "python3.12",
"python3.11", "python3.11",
"python3.10",
"python3", "python3",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python313\python.exe", "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python313\python.exe",
"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:\Python313\python.exe", "C:\Python313\python.exe",
"C:\Python312\python.exe", "C:\Python312\python.exe",
"C:\Python311\python.exe", "C:\Python311\python.exe"
"C:\Python310\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.10 or when Python executable not found # Exit on lower versions than Python 3.11 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.10 or higher is installed and available in the system PATH." -Level 'ERROR' Write-Log "No suitable Python executable found. Please ensure that Python 3.11 or higher is installed and available in the system PATH." -Level 'ERROR'
Exit 1 Exit 1
} }
+4 -4
View File
@@ -25,7 +25,7 @@ function check_installed_python() {
exit 2 exit 2
fi fi
for v in 13 12 11 10 for v in 13 12 11
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.10 or newer installed." echo "No usable python found. Please make sure to have python3.11 or newer installed."
exit 1 exit 1
} }
@@ -257,7 +257,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.10 - 3.13, pip, virtualenv, ta-lib you can continue." echo "If you have Python version 3.11 - 3.13, 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
@@ -284,7 +284,7 @@ function help() {
echo " -p,--plot Install dependencies for Plotting scripts." echo " -p,--plot Install dependencies for Plotting scripts."
} }
# Verify if 3.10+ is installed # Verify if 3.11+ is installed
check_installed_python check_installed_python
case $* in case $* in
+34 -2
View File
@@ -4,7 +4,7 @@ import logging
import platform import platform
import re import re
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, Mock, PropertyMock from unittest.mock import MagicMock, Mock, PropertyMock
@@ -126,7 +126,7 @@ def get_args(args):
def generate_trades_history(n_rows, start_date: datetime | None = None, days=5): def generate_trades_history(n_rows, start_date: datetime | None = None, days=5):
np.random.seed(42) np.random.seed(42)
if not start_date: if not start_date:
start_date = datetime(2020, 1, 1, tzinfo=timezone.utc) start_date = datetime(2020, 1, 1, tzinfo=UTC)
# Generate random data # Generate random data
end_date = start_date + timedelta(days=days) end_date = start_date + timedelta(days=days)
@@ -258,6 +258,7 @@ def patch_exchange(
"._supported_trading_mode_margin_pairs", "._supported_trading_mode_margin_pairs",
PropertyMock( PropertyMock(
return_value=[ return_value=[
(TradingMode.SPOT, MarginMode.NONE),
(TradingMode.MARGIN, MarginMode.CROSS), (TradingMode.MARGIN, MarginMode.CROSS),
(TradingMode.MARGIN, MarginMode.ISOLATED), (TradingMode.MARGIN, MarginMode.ISOLATED),
(TradingMode.FUTURES, MarginMode.CROSS), (TradingMode.FUTURES, MarginMode.CROSS),
@@ -3405,4 +3406,35 @@ def leverage_tiers():
"maintAmt": 654500.0, "maintAmt": 654500.0,
}, },
], ],
"TIA/USDT:USDT": [
# Okx tier - these have a gap between maxNotional and the next minNotional
{
"minNotional": 0.0,
"maxNotional": 6500.0,
"maintenanceMarginRate": 0.0065,
"maxLeverage": 50.0,
"maintAmt": None,
},
{
"minNotional": 6501.0,
"maxNotional": 12000.0,
"maintenanceMarginRate": 0.01,
"maxLeverage": 40.0,
"maintAmt": None,
},
{
"minNotional": 12001.0,
"maxNotional": 25000.0,
"maintenanceMarginRate": 0.015,
"maxLeverage": 20.0,
"maintAmt": None,
},
{
"minNotional": 25001.0,
"maxNotional": 50000.0,
"maintenanceMarginRate": 0.02,
"maxLeverage": 18.18,
"maintAmt": None,
},
],
} }
+12 -12
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from freqtrade.persistence.models import Order, Trade from freqtrade.persistence.models import Order, Trade
@@ -43,7 +43,7 @@ def mock_trade_1(fee, is_short: bool):
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
is_open=True, is_open=True,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_date=datetime.now(tz=UTC) - timedelta(minutes=17),
open_rate=0.123, open_rate=0.123,
exchange="binance", exchange="binance",
strategy="StrategyTestV3", strategy="StrategyTestV3",
@@ -106,8 +106,8 @@ def mock_trade_2(fee, is_short: bool):
timeframe=5, timeframe=5,
enter_tag="TEST1", enter_tag="TEST1",
exit_reason="sell_signal", exit_reason="sell_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), close_date=datetime.now(tz=UTC) - timedelta(minutes=2),
is_short=is_short, is_short=is_short,
) )
o = Order.parse_from_ccxt_object(mock_order_2(is_short), "ETC/BTC", entry_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_2(is_short), "ETC/BTC", entry_side(is_short))
@@ -168,8 +168,8 @@ def mock_trade_3(fee, is_short: bool):
strategy="StrategyTestV3", strategy="StrategyTestV3",
timeframe=5, timeframe=5,
exit_reason="roi", exit_reason="roi",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc), close_date=datetime.now(tz=UTC),
is_short=is_short, is_short=is_short,
) )
o = Order.parse_from_ccxt_object(mock_order_3(is_short), "XRP/BTC", entry_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_3(is_short), "XRP/BTC", entry_side(is_short))
@@ -205,7 +205,7 @@ def mock_trade_4(fee, is_short: bool):
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,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14), open_date=datetime.now(tz=UTC) - timedelta(minutes=14),
is_open=True, is_open=True,
open_rate=0.123, open_rate=0.123,
exchange="binance", exchange="binance",
@@ -260,7 +260,7 @@ def mock_trade_5(fee, is_short: bool):
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,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12), open_date=datetime.now(tz=UTC) - timedelta(minutes=12),
is_open=True, is_open=True,
open_rate=0.123, open_rate=0.123,
exchange="binance", exchange="binance",
@@ -316,7 +316,7 @@ def mock_trade_6(fee, is_short: bool):
stake_amount=0.001, stake_amount=0.001,
amount=2.0, amount=2.0,
amount_requested=2.0, amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5), open_date=datetime.now(tz=UTC) - timedelta(minutes=5),
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
is_open=True, is_open=True,
@@ -410,7 +410,7 @@ def short_trade(fee):
strategy="DefaultStrategy", strategy="DefaultStrategy",
timeframe=5, timeframe=5,
exit_reason="sell_signal", exit_reason="sell_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
# close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), # close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
is_short=True, is_short=True,
) )
@@ -500,8 +500,8 @@ def leverage_trade(fee):
strategy="DefaultStrategy", strategy="DefaultStrategy",
timeframe=5, timeframe=5,
exit_reason="sell_signal", exit_reason="sell_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=300), open_date=datetime.now(tz=UTC) - timedelta(minutes=300),
close_date=datetime.now(tz=timezone.utc), close_date=datetime.now(tz=UTC),
interest_rate=0.0005, interest_rate=0.0005,
) )
o = Order.parse_from_ccxt_object(leverage_order(), "DOGE/BTC", "sell") o = Order.parse_from_ccxt_object(leverage_order(), "DOGE/BTC", "sell")
+11 -11
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from freqtrade.persistence.models import Order, Trade from freqtrade.persistence.models import Order, Trade
@@ -55,8 +55,8 @@ def mock_trade_usdt_1(fee, is_short: bool):
stake_amount=20.0, stake_amount=20.0,
amount=2.0, amount=2.0,
amount_requested=2.0, amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=20), open_date=datetime.now(tz=UTC) - timedelta(days=2, minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=5), close_date=datetime.now(tz=UTC) - timedelta(days=2, minutes=5),
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
is_open=False, is_open=False,
@@ -127,8 +127,8 @@ def mock_trade_usdt_2(fee, is_short: bool):
timeframe=5, timeframe=5,
enter_tag="TEST1", enter_tag="TEST1",
exit_reason="exit_signal", exit_reason="exit_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), close_date=datetime.now(tz=UTC) - timedelta(minutes=2),
is_short=is_short, is_short=is_short,
) )
o = Order.parse_from_ccxt_object(mock_order_usdt_2(is_short), "NEO/USDT", entry_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_usdt_2(is_short), "NEO/USDT", entry_side(is_short))
@@ -190,8 +190,8 @@ def mock_trade_usdt_3(fee, is_short: bool):
timeframe=5, timeframe=5,
enter_tag="TEST3", enter_tag="TEST3",
exit_reason="roi", exit_reason="roi",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc), close_date=datetime.now(tz=UTC),
is_short=is_short, is_short=is_short,
) )
o = Order.parse_from_ccxt_object(mock_order_usdt_3(is_short), "XRP/USDT", entry_side(is_short)) o = Order.parse_from_ccxt_object(mock_order_usdt_3(is_short), "XRP/USDT", entry_side(is_short))
@@ -228,7 +228,7 @@ def mock_trade_usdt_4(fee, is_short: bool):
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,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14), open_date=datetime.now(tz=UTC) - timedelta(minutes=14),
is_open=True, is_open=True,
open_rate=2.0, open_rate=2.0,
exchange="binance", exchange="binance",
@@ -280,7 +280,7 @@ def mock_trade_usdt_5(fee, is_short: bool):
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,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12), open_date=datetime.now(tz=UTC) - timedelta(minutes=12),
is_open=True, is_open=True,
open_rate=2.0, open_rate=2.0,
exchange="binance", exchange="binance",
@@ -332,7 +332,7 @@ def mock_trade_usdt_6(fee, is_short: bool):
stake_amount=20.0, stake_amount=20.0,
amount=2.0, amount=2.0,
amount_requested=2.0, amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5), open_date=datetime.now(tz=UTC) - timedelta(minutes=5),
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
is_open=True, is_open=True,
@@ -374,7 +374,7 @@ def mock_trade_usdt_7(fee, is_short: bool):
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
is_open=True, is_open=True,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_date=datetime.now(tz=UTC) - timedelta(minutes=17),
open_rate=2.0, open_rate=2.0,
exchange="binance", exchange="binance",
strategy="StrategyTestV2", strategy="StrategyTestV2",
+15 -15
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
from zipfile import ZipFile from zipfile import ZipFile
@@ -182,19 +182,19 @@ def test_extract_trades_of_period(testdatadir):
"profit_abs": [0.0, 1, -2, -5], "profit_abs": [0.0, 1, -2, -5],
"open_date": to_datetime( "open_date": to_datetime(
[ [
datetime(2017, 11, 13, 15, 40, 0, tzinfo=timezone.utc), datetime(2017, 11, 13, 15, 40, 0, tzinfo=UTC),
datetime(2017, 11, 14, 9, 41, 0, tzinfo=timezone.utc), datetime(2017, 11, 14, 9, 41, 0, tzinfo=UTC),
datetime(2017, 11, 14, 14, 20, 0, tzinfo=timezone.utc), datetime(2017, 11, 14, 14, 20, 0, tzinfo=UTC),
datetime(2017, 11, 15, 3, 40, 0, tzinfo=timezone.utc), datetime(2017, 11, 15, 3, 40, 0, tzinfo=UTC),
], ],
utc=True, utc=True,
), ),
"close_date": to_datetime( "close_date": to_datetime(
[ [
datetime(2017, 11, 13, 16, 40, 0, tzinfo=timezone.utc), datetime(2017, 11, 13, 16, 40, 0, tzinfo=UTC),
datetime(2017, 11, 14, 10, 41, 0, tzinfo=timezone.utc), datetime(2017, 11, 14, 10, 41, 0, tzinfo=UTC),
datetime(2017, 11, 14, 15, 25, 0, tzinfo=timezone.utc), datetime(2017, 11, 14, 15, 25, 0, tzinfo=UTC),
datetime(2017, 11, 15, 3, 55, 0, tzinfo=timezone.utc), datetime(2017, 11, 15, 3, 55, 0, tzinfo=UTC),
], ],
utc=True, utc=True,
), ),
@@ -203,10 +203,10 @@ def test_extract_trades_of_period(testdatadir):
trades1 = extract_trades_of_period(data, trades) trades1 = extract_trades_of_period(data, trades)
# First and last trade are dropped as they are out of range # First and last trade are dropped as they are out of range
assert len(trades1) == 2 assert len(trades1) == 2
assert trades1.iloc[0].open_date == datetime(2017, 11, 14, 9, 41, 0, tzinfo=timezone.utc) assert trades1.iloc[0].open_date == datetime(2017, 11, 14, 9, 41, 0, tzinfo=UTC)
assert trades1.iloc[0].close_date == datetime(2017, 11, 14, 10, 41, 0, tzinfo=timezone.utc) assert trades1.iloc[0].close_date == datetime(2017, 11, 14, 10, 41, 0, tzinfo=UTC)
assert trades1.iloc[-1].open_date == datetime(2017, 11, 14, 14, 20, 0, tzinfo=timezone.utc) assert trades1.iloc[-1].open_date == datetime(2017, 11, 14, 14, 20, 0, tzinfo=UTC)
assert trades1.iloc[-1].close_date == datetime(2017, 11, 14, 15, 25, 0, tzinfo=timezone.utc) assert trades1.iloc[-1].close_date == datetime(2017, 11, 14, 15, 25, 0, tzinfo=UTC)
def test_analyze_trade_parallelism(testdatadir): def test_analyze_trade_parallelism(testdatadir):
@@ -293,7 +293,7 @@ def test_combined_dataframes_with_rel_mean(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"] pairs = ["ETH/BTC", "ADA/BTC"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m") data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
df = combined_dataframes_with_rel_mean( df = combined_dataframes_with_rel_mean(
data, datetime(2018, 1, 12, tzinfo=timezone.utc), datetime(2018, 1, 28, tzinfo=timezone.utc) data, datetime(2018, 1, 12, tzinfo=UTC), datetime(2018, 1, 28, tzinfo=UTC)
) )
assert isinstance(df, DataFrame) assert isinstance(df, DataFrame)
assert "ETH/BTC" not in df.columns assert "ETH/BTC" not in df.columns
@@ -596,7 +596,7 @@ def test_calculate_max_drawdown_abs(profits, relative, highd, lowdays, result, r
[1000, 500, 1000, 11000, 10000] # absolute results [1000, 500, 1000, 11000, 10000] # absolute results
[1000, 50%, 0%, 0%, ~9%] # Relative drawdowns [1000, 50%, 0%, 0%, ~9%] # Relative drawdowns
""" """
init_date = datetime(2020, 1, 1, tzinfo=timezone.utc) init_date = datetime(2020, 1, 1, tzinfo=UTC)
dates = [init_date + timedelta(days=i) for i in range(len(profits))] dates = [init_date + timedelta(days=i) for i in range(len(profits))]
df = DataFrame(zip(profits, dates, strict=False), columns=["profit_abs", "open_date"]) df = DataFrame(zip(profits, dates, strict=False), columns=["profit_abs", "open_date"])
# sort by profit and reset index # sort by profit and reset index
+8 -8
View File
@@ -1,7 +1,7 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103 # pragma pylint: disable=missing-docstring, protected-access, C0103
import re import re
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
@@ -165,19 +165,19 @@ def test_datahandler_ohlcv_data_min_max(testdatadir):
# Empty pair # Empty pair
min_max = dh.ohlcv_data_min_max("UNITTEST/BTC", "8m", "spot") min_max = dh.ohlcv_data_min_max("UNITTEST/BTC", "8m", "spot")
assert len(min_max) == 3 assert len(min_max) == 3
assert min_max[0] == datetime.fromtimestamp(0, tz=timezone.utc) assert min_max[0] == datetime.fromtimestamp(0, tz=UTC)
assert min_max[0] == min_max[1] assert min_max[0] == min_max[1]
# Empty pair2 # Empty pair2
min_max = dh.ohlcv_data_min_max("NOPAIR/XXX", "41m", "spot") min_max = dh.ohlcv_data_min_max("NOPAIR/XXX", "41m", "spot")
assert len(min_max) == 3 assert len(min_max) == 3
assert min_max[0] == datetime.fromtimestamp(0, tz=timezone.utc) assert min_max[0] == datetime.fromtimestamp(0, tz=UTC)
assert min_max[0] == min_max[1] assert min_max[0] == min_max[1]
# Existing pair ... # Existing pair ...
min_max = dh.ohlcv_data_min_max("UNITTEST/BTC", "1m", "spot") min_max = dh.ohlcv_data_min_max("UNITTEST/BTC", "1m", "spot")
assert len(min_max) == 3 assert len(min_max) == 3
assert min_max[0] == datetime(2017, 11, 4, 23, 2, tzinfo=timezone.utc) assert min_max[0] == datetime(2017, 11, 4, 23, 2, tzinfo=UTC)
assert min_max[1] == datetime(2017, 11, 14, 22, 59, tzinfo=timezone.utc) assert min_max[1] == datetime(2017, 11, 14, 22, 59, tzinfo=UTC)
def test_datahandler__check_empty_df(testdatadir, caplog): def test_datahandler__check_empty_df(testdatadir, caplog):
@@ -467,14 +467,14 @@ def test_datahandler_trades_data_min_max(testdatadir):
# Empty pair # Empty pair
min_max = dh.trades_data_min_max("NADA/ETH", TradingMode.SPOT) min_max = dh.trades_data_min_max("NADA/ETH", TradingMode.SPOT)
assert len(min_max) == 3 assert len(min_max) == 3
assert min_max[0] == datetime.fromtimestamp(0, tz=timezone.utc) assert min_max[0] == datetime.fromtimestamp(0, tz=UTC)
assert min_max[0] == min_max[1] assert min_max[0] == min_max[1]
# Existing pair ... # Existing pair ...
min_max = dh.trades_data_min_max("XRP/ETH", TradingMode.SPOT) min_max = dh.trades_data_min_max("XRP/ETH", TradingMode.SPOT)
assert len(min_max) == 3 assert len(min_max) == 3
assert min_max[0] == datetime(2019, 10, 11, 0, 0, 11, 620000, tzinfo=timezone.utc) assert min_max[0] == datetime(2019, 10, 11, 0, 0, 11, 620000, tzinfo=UTC)
assert min_max[1] == datetime(2019, 10, 13, 11, 19, 28, 844000, tzinfo=timezone.utc) assert min_max[1] == datetime(2019, 10, 13, 11, 19, 28, 844000, tzinfo=UTC)
def test_gethandlerclass(): def test_gethandlerclass():

Some files were not shown because too many files have changed in this diff Show More