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

View File

@@ -1,5 +1,10 @@
<!-- 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)
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

View File

@@ -25,7 +25,7 @@ jobs:
strategy:
matrix:
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:
- uses: actions/checkout@v4
@@ -38,7 +38,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true
@@ -160,7 +160,7 @@ jobs:
strategy:
matrix:
os: [ "macos-14", "macos-15" ]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
@@ -174,7 +174,7 @@ jobs:
check-latest: true
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true
@@ -288,7 +288,7 @@ jobs:
strategy:
matrix:
os: [ windows-latest ]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
@@ -301,7 +301,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true
@@ -449,7 +449,7 @@ jobs:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true

View File

@@ -55,14 +55,6 @@ jobs:
run: |
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
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0

29
.github/workflows/zizmor.yml vendored Normal file
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

View File

@@ -21,16 +21,17 @@ repos:
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.16.1"
rev: "v1.17.0"
hooks:
- id: mypy
exclude: build_helpers
additional_dependencies:
- types-cachetools==6.0.0.20250525
- types-cachetools==6.1.0.20250717
- types-filelock==3.2.7
- types-requests==2.32.4.20250611
- 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
# stages: [push]
@@ -43,7 +44,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.12.1'
rev: 'v0.12.5'
hooks:
- id: ruff
- id: ruff-format
@@ -69,7 +70,7 @@ repos:
)$
- repo: https://github.com/stefmolin/exif-stripper
rev: 1.0.0
rev: 1.1.0
hooks:
- id: strip-exif

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
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONFAULTHANDLER 1
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONFAULTHANDLER=1
ENV PATH=/home/ftuser/.local/bin:$PATH
ENV FT_APP_ENV="docker"
@@ -21,7 +21,7 @@ RUN mkdir /freqtrade \
WORKDIR /freqtrade
# Install dependencies
FROM base as python-deps
FROM base AS python-deps
RUN apt-get update \
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \
@@ -30,7 +30,7 @@ RUN apt-get update \
# Install TA-lib
COPY build_helpers/* /tmp/
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
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
# 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
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

View File

@@ -64,7 +64,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
## 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] **Dry-run**: Run the bot without paying money.
- [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.
- `/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_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`).
- `/fx <trade_id>|all`: Alias to `/forceexit`
- `/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.
- `/version`: Show version.
## Development 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
- [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/)
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)

View File

@@ -16,10 +16,12 @@ with require_dev.open("r") as rfile:
with require.open("r") as rfile:
requirements.extend(rfile.readlines())
# Extract types only
type_reqs = [
r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy")
]
# Extract relevant types only
supported = ("types-", "SQLAlchemy", "scipy-stubs")
# 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:
f = yaml.load(file, Loader=yaml.SafeLoader)

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
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONFAULTHANDLER 1
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONFAULTHANDLER=1
ENV PATH=/home/ftuser/.local/bin:$PATH
ENV FT_APP_ENV="docker"
@@ -22,7 +22,7 @@ RUN mkdir /freqtrade \
WORKDIR /freqtrade
# Install dependencies
FROM base as python-deps
FROM base AS python-deps
RUN apt-get update \
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
&& 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
# 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
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

View File

@@ -321,6 +321,7 @@ It contains some useful key metrics about performance of your strategy on backte
| SQN | 2.45 |
| Profit factor | 1.11 |
| Expectancy (Ratio) | -0.15 (-0.05) |
| Avg. daily profit | 0.0001 BTC |
| Avg. stake amount | 0.001 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.
- `SQN`: System Quality Number (SQN) - by Van Tharp.
- `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.
- `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 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).

View File

@@ -339,13 +339,13 @@ This needs to be configured like this:
```json
"exchange": {
"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",
// ...
}
```
* 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.
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.
* 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
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.

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.
* 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
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).

View File

@@ -87,7 +87,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
Alternatively
- Python 3.10+
- Python 3.11+
- pip (pip3)
- git
- TA-Lib

View File

@@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
!!! Note
Python3.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.
!!! Warning "Up-to-date clock"
@@ -42,7 +42,7 @@ These requirements apply to both [Script Installation](#script-installation) and
### 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/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
@@ -54,7 +54,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
OS Specific steps are listed first, the common section below is necessary for all systems.
!!! 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"
#### Install necessary dependencies
@@ -179,7 +179,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
** --install **
With this option, the script will install the bot and most dependencies:
You will need to have git and python3.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`
* Setup your virtualenv under `.venv/`

View File

@@ -1,6 +1,6 @@
markdown==3.8.2
mkdocs==1.6.1
mkdocs-material==9.6.14
mkdocs-material==9.6.16
mdx_truly_sane_lists==1.3
pymdown-extensions==10.16
jinja2==3.1.6

View File

@@ -174,17 +174,27 @@ class AwesomeStrategy(IStrategy):
## Enter Tag
When your strategy has multiple buy signals, you can name the signal that triggered.
Then you can access your buy signal on `custom_exit`
When your strategy has multiple entry signals, you can name the signal that triggered.
Then you can access your entry signal on `custom_exit`
```python
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['rsi'] < 35) &
(dataframe['volume'] > 0)
),
['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi')
signal_rsi
| signal_bblower
# ... additional signals to enter a long position
)
& (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
@@ -192,14 +202,17 @@ def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_r
current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
if trade.enter_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
return 'sell_signal_rsi'
if "long_signal_rsi" in trade.enter_tag and last_candle["rsi"] > 80:
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
```
!!! 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
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
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['rsi'] > 70) &
(dataframe['volume'] > 0)
),
['exit_long', 'exit_tag']] = (1, 'exit_rsi')
rsi_exit_signal
| ema_exit_signal
# ... additional signals to exit a long position
) &
(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
```
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
`exit_reason` is limited to 100 characters, remaining data will be truncated.

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.
| **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_[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
| `/balance` | Show bot managed balance per currency
| `/balance full` | Show account balance per currency
@@ -309,6 +310,8 @@ current max
### /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.
> **ROI:** Close trades

View File

@@ -117,9 +117,9 @@ Different payloads can be configured for different events. Not all fields are ne
## 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:
* `trade_id`
@@ -162,31 +162,9 @@ Possible parameters are:
* `current_rate`
* `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.
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.
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`
@@ -195,34 +173,9 @@ Possible parameters are:
* `direction`
* `leverage`
* `gain`
* `limit`
* `amount`
* `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`
* `amount`
* `open_rate`
* `current_rate`
* `profit_amount`
* `profit_ratio`
@@ -230,10 +183,14 @@ Possible parameters are:
* `base_currency`
* `quote_currency`
* `fiat_currency`
* `enter_tag`
* `exit_reason`
* `order_type`
* `open_date`
* `close_date`
* `sub_trade`
* `is_final_exit`
### Exit cancel
@@ -246,7 +203,7 @@ Possible parameters are:
* `direction`
* `leverage`
* `gain`
* `limit`
* `order_rate`
* `amount`
* `open_rate`
* `current_rate`

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.
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
@@ -42,7 +42,7 @@ cd freqtrade
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.
Other versions must be downloaded from the above link.

View File

@@ -1,6 +1,6 @@
"""Freqtrade bot"""
__version__ = "2025.6"
__version__ = "2025.7"
if "dev" in __version__:
from pathlib import Path

View File

@@ -3,7 +3,7 @@
__main__.py for Freqtrade
To launch Freqtrade as a module
> python -m freqtrade (with Python >= 3.10)
> python -m freqtrade (with Python >= 3.11)
"""
from freqtrade import main

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
NO_CONF_REQURIED = [
"backtest-filter",
"backtesting-show",
"convert-data",
"convert-trade-data",
"download-data",
"list-timeframes",
"hyperopt-list",
"hyperopt-show",
"list-data",
"list-freqaimodels",
"list-hyperoptloss",
"list-markets",
"list-pairs",
"list-strategies",
"list-freqaimodels",
"list-hyperoptloss",
"list-data",
"hyperopt-list",
"hyperopt-show",
"backtest-filter",
"list-timeframes",
"plot-dataframe",
"plot-profit",
"show-trades",
"trades-to-ohlcv",
"install-ui",
"strategy-updater",
"trades-to-ohlcv",
]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
@@ -311,8 +313,6 @@ class Arguments:
# (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting)
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:
user_dir = parsed_arg.user_data_dir
else:
@@ -325,7 +325,9 @@ class Arguments:
else:
# Else use "config.json".
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]
return parsed_arg

View File

@@ -18,10 +18,7 @@ from freqtrade.constants import Config
from freqtrade.enums import (
NON_UTIL_MODES,
TRADE_MODES,
CandleType,
MarginMode,
RunMode,
TradingMode,
)
from freqtrade.exceptions import OperationalException
from freqtrade.loggers import setup_logging
@@ -397,11 +394,6 @@ class Configuration:
self._args_to_config(
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(
config, argname="candle_types", logstring="Detected --candle-types: {}"
)

View File

@@ -4,9 +4,8 @@ This module contains the argument manager class
import logging
import re
from datetime import datetime, timezone
from typing_extensions import Self
from datetime import UTC, datetime
from typing import Self
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.exceptions import ConfigurationError
@@ -151,9 +150,7 @@ class TimeRange:
starts = rvals[index]
if stype[0] == "date" and len(starts) == 8:
start = int(
datetime.strptime(starts, "%Y%m%d")
.replace(tzinfo=timezone.utc)
.timestamp()
datetime.strptime(starts, "%Y%m%d").replace(tzinfo=UTC).timestamp()
)
elif len(starts) == 13:
start = int(starts) // 1000
@@ -164,9 +161,7 @@ class TimeRange:
stops = rvals[index]
if stype[1] == "date" and len(stops) == 8:
stop = int(
datetime.strptime(stops, "%Y%m%d")
.replace(tzinfo=timezone.utc)
.timestamp()
datetime.strptime(stops, "%Y%m%d").replace(tzinfo=UTC).timestamp()
)
elif len(stops) == 13:
stop = int(stops) // 1000

View File

@@ -5,7 +5,7 @@ Helpers when analyzing backtest data
import logging
import zipfile
from copy import copy
from datetime import datetime, timezone
from datetime import UTC, datetime
from io import BytesIO, StringIO
from pathlib import Path
from typing import Any, Literal
@@ -324,7 +324,7 @@ def find_existing_backtest_stats(
if min_backtest_date is not None:
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:
# Do not use a cached result for this strategy as first result is too old.
del run_ids[strategy_name]

View File

@@ -7,7 +7,7 @@ Common Interface for bot and strategy to access data.
import logging
from collections import deque
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
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!)
"""
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
def _set_producer_pairs(self, pairlist: list[str], producer_name: str = "default"):
@@ -131,7 +131,7 @@ class DataProvider:
"data": {
"key": pair_key,
"df": dataframe.tail(1),
"la": datetime.now(timezone.utc),
"la": datetime.now(UTC),
},
}
self.__rpc.send_msg(msg)
@@ -164,7 +164,7 @@ class DataProvider:
if producer_name not in self.__producer_pairs_df:
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)
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 producer_name not in self.__producer_pairs_df:
# 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 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)
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
# We have it, return this data
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:
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
else:
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
return df, date
else:
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
@property
def runmode(self) -> RunMode:

View File

@@ -8,7 +8,7 @@ import logging
import re
from abc import ABC, abstractmethod
from copy import deepcopy
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from pandas import DataFrame, to_datetime
@@ -118,8 +118,8 @@ class IDataHandler(ABC):
df = self._ohlcv_load(pair, timeframe, None, candle_type)
if df.empty:
return (
datetime.fromtimestamp(0, tz=timezone.utc),
datetime.fromtimestamp(0, tz=timezone.utc),
datetime.fromtimestamp(0, tz=UTC),
datetime.fromtimestamp(0, tz=UTC),
0,
)
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)
if df.empty:
return (
datetime.fromtimestamp(0, tz=timezone.utc),
datetime.fromtimestamp(0, tz=timezone.utc),
datetime.fromtimestamp(0, tz=UTC),
datetime.fromtimestamp(0, tz=UTC),
0,
)
return (

View File

@@ -174,12 +174,18 @@ def calculate_underwater(
@dataclass()
class DrawDownResult:
# Max drawdown fields
drawdown_abs: float = 0.0
high_date: pd.Timestamp = None
low_date: pd.Timestamp = None
high_value: float = 0.0
low_value: 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(
@@ -191,29 +197,31 @@ def calculate_max_drawdown(
relative: bool = False,
) -> DrawDownResult:
"""
Calculate max drawdown and the corresponding close dates
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
Calculate max drawdown and current drawdown with corresponding dates
: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 value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
: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
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.
"""
if len(trades) == 0:
raise ValueError("Trade dataframe empty.")
profit_results = trades.sort_values(date_col).reset_index(drop=True)
max_drawdown_df = _calc_drawdown_series(
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
)
# Calculate maximum drawdown
idxmin = (
max_drawdown_df["drawdown_relative"].idxmax()
if relative
else max_drawdown_df["drawdown"].idxmin()
)
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
high_date = profit_results.loc[high_idx, 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"]
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(
# Max drawdown
drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]),
high_date=high_date,
low_date=low_date,
high_value=high_val,
low_value=low_val,
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,
)

View File

@@ -43,4 +43,6 @@ from freqtrade.exchange.idex import Idex
from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.kucoin import Kucoin
from freqtrade.exchange.lbank import Lbank
from freqtrade.exchange.luno import Luno
from freqtrade.exchange.modetrade import Modetrade
from freqtrade.exchange.okx import Okx

View File

@@ -1,7 +1,7 @@
"""Binance exchange subclass"""
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
import ccxt
@@ -63,7 +63,7 @@ class Binance(Exchange):
}
_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.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED),
@@ -160,7 +160,7 @@ class Binance(Exchange):
since_ms = x[3][0][0]
logger.info(
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:
logger.warning(
@@ -399,7 +399,7 @@ class Binance(Exchange):
trades = await self._api_async.fetch_trades(
pair,
params={
self._trades_pagination_arg: "0",
self._ft_has["trades_pagination_arg"]: "0",
},
limit=5,
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
"""Bitpanda exchange subclass"""
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from freqtrade.exchange import Exchange
@@ -34,5 +34,5 @@ class Bitpanda(Exchange):
:param pair: Pair the order is for
: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)

View File

@@ -1,8 +1,5 @@
"""Bybit exchange subclass"""
import logging
from datetime import datetime, timedelta
from typing import Any
import ccxt
@@ -12,6 +9,7 @@ from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalExcep
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.misc import deep_merge_dicts
logger = logging.getLogger(__name__)
@@ -64,9 +62,9 @@ class Bybit(Exchange):
}
_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.ISOLATED)
]
@property
@@ -76,14 +74,11 @@ class Bybit(Exchange):
config = {}
if self.trading_mode == TradingMode.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
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
def additional_exchange_init(self) -> None:
"""
@@ -182,18 +177,36 @@ class Bybit(Exchange):
PERPETUAL:
bybit:
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 = (
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
- (Extra Margin Added/Contract Quantity))
Short:
Position Entry Price - [
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
]
)
Short USDC:
Liquidation Price = (
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
+ (Extra Margin Added/Contract Quantity))
Position Entry Price + [
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
]
)
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 open_rate: Entry price of position

View File

@@ -9,7 +9,7 @@ import logging
import signal
from collections.abc import Coroutine, Generator
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from math import floor, isnan
from threading import Lock
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_partial_candle": True,
"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
"ohlcv_volume_currency": "base", # "base" or "quote"
"tickers_have_quoteVolume": True,
@@ -168,7 +169,8 @@ class Exchange:
_ft_has_futures: FtHas = {}
_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__(
@@ -197,7 +199,26 @@ class Exchange:
self.loop = self._init_async_loop()
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.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
self._pairs_last_refresh_time: dict[PairWithTimeframe, int] = {}
@@ -227,33 +248,17 @@ class Exchange:
if config["dry_run"]:
logger.info("Instance is running with dry_run enabled")
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
self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
self.margin_mode: MarginMode = (
MarginMode(config.get("margin_mode")) if config.get("margin_mode") else MarginMode.NONE
# Don't remove exchange credentials for dry-run or if always_require_api_keys is set
remove_exchange_credentials(
exchange_conf,
not self._ft_has["always_require_api_keys"] and config.get("dry_run", False),
)
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
# 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)
self.log_responses = exchange_conf.get("log_responses", False)
# Assign this directly for easy access
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
ccxt_config = self._ccxt_config
ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config)
@@ -289,10 +294,6 @@ class Exchange:
# Initial markets load
self.reload_markets(True, load_leverage_tiers=False)
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:
self.fill_leverage_tiers()
@@ -331,6 +332,12 @@ class Exchange:
asyncio.set_event_loop(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:
# Check if timeframe is available
self.validate_timeframes(config.get("timeframe"))
@@ -345,6 +352,8 @@ class Exchange:
self.validate_orderflow(config["exchange"])
self.validate_freqai(config)
self._set_startup_candle_count(config)
def _init_ccxt(
self, exchange_config: dict[str, Any], sync: bool, ccxt_kwargs: dict[str, Any]
) -> ccxt.Exchange:
@@ -657,7 +666,7 @@ class Exchange:
if isinstance(markets, Exception):
raise markets
return None
except asyncio.TimeoutError as e:
except TimeoutError as e:
logger.warning("Could not load markets. Reason: %s", e)
raise TemporaryError from e
@@ -881,6 +890,20 @@ class Exchange:
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:
"""
Get parameter value from _ft_has
@@ -2208,7 +2231,7 @@ class Exchange:
_params = params if params else {}
my_trades = self._api.fetch_my_trades(
pair,
int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
int((since.replace(tzinfo=UTC).timestamp() - 5) * 1000),
params=_params,
)
matched_trades = [trade for trade in my_trades if trade["order"] == order_id]
@@ -2584,10 +2607,12 @@ class Exchange:
if ticks and cache:
idx = -2 if drop_incomplete and len(ticks) > 1 else -1
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(
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 (pair, timeframe, c_type) in self._klines:
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)
"""
try:
trades_limit = self._max_trades_limit
trades_limit = self._ft_has["trades_limit"]
# fetch trades asynchronously
if params:
logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
@@ -3039,7 +3064,7 @@ class Exchange:
"""
if not trades:
return None
if self._trades_pagination == "id":
if self._ft_has["trades_pagination"] == "id":
return trades[-1].get("id")
else:
return trades[-1].get("timestamp")
@@ -3057,7 +3082,7 @@ class Exchange:
) -> tuple[str, list[list]]:
"""
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 since: Since as integer timestamp in milliseconds
:param until: Until as integer timestamp in milliseconds
@@ -3083,7 +3108,7 @@ class Exchange:
while True:
try:
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:
trades.extend(t[x])
@@ -3111,7 +3136,7 @@ class Exchange:
) -> tuple[str, list[list]]:
"""
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 since: Since as integer timestamp in milliseconds
:param until: Until as integer timestamp in milliseconds
@@ -3165,9 +3190,9 @@ class Exchange:
until = ccxt.Exchange.milliseconds()
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)
elif self._trades_pagination == "id":
elif self._ft_has["trades_pagination"] == "id":
return await self._async_get_trade_history_id(
pair=pair, since=since, until=until, from_id=from_id
)
@@ -3335,7 +3360,7 @@ class Exchange:
if not filename.parent.is_dir():
filename.parent.mkdir(parents=True)
data = {
"updated": datetime.now(timezone.utc),
"updated": datetime.now(UTC),
"data": tiers,
}
file_dump_json(filename, data)
@@ -3357,7 +3382,7 @@ class Exchange:
updated = tiers.get("updated")
if 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.")
return None
return tiers.get("data")
@@ -3416,17 +3441,26 @@ class Exchange:
# Find the appropriate tier based on stake_amount
prior_max_lev = None
for tier in pair_tiers:
# Adjust notional by leverage to do a proper comparison
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
max_stake = tier["maxNotional"] / tier["maxLeverage"]
prior_max_lev = tier["maxLeverage"]
# Adjust notional by leverage to do a proper comparison
if min_stake <= stake_amount <= max_stake:
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
if stake_amount > max_stake:
# 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(
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"])
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))
mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)

View File

@@ -24,6 +24,7 @@ class FtHas(TypedDict, total=False):
ohlcv_require_since: bool
ohlcv_volume_currency: str
ohlcv_candle_limit_per_timeframe: dict[str, int]
always_require_api_keys: bool
# Tickers
tickers_have_quoteVolume: bool
tickers_have_percentage: bool

View File

@@ -3,7 +3,7 @@ Exchange support utils
"""
import inspect
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from math import ceil, floor, isnan
from typing import Any
@@ -27,7 +27,7 @@ from freqtrade.exchange.common import (
SUPPORTED_EXCHANGES,
)
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
@@ -110,7 +110,7 @@ def _build_exchange_list_entry(
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
}
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}
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:
date = datetime.now(timezone.utc)
date = datetime.now(UTC)
tf_min = timeframe_to_minutes(timeframe)
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
import ccxt
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)
"""
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
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)
"""
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
return dt_from_ts(new_timestamp)

View File

@@ -55,10 +55,10 @@ class Gate(Exchange):
}
_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.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED)
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
@retrier

View File

@@ -28,6 +28,7 @@ class Hyperliquid(Exchange):
"stoploss_on_exchange": False,
"exchange_has_overrides": {"fetchTrades": False},
"marketOrderRequiresPrice": True,
"ws_enabled": True,
}
_ft_has_futures: FtHas = {
"stoploss_on_exchange": True,
@@ -40,7 +41,8 @@ class Hyperliquid(Exchange):
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.FUTURES, MarginMode.ISOLATED)
(TradingMode.SPOT, MarginMode.NONE),
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
@property

View File

@@ -35,7 +35,7 @@ class Kraken(Exchange):
}
_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.FUTURES, MarginMode.CROSS)
]

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
}

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),
# ]

View File

@@ -49,7 +49,7 @@ class Okx(Exchange):
}
_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.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED),

View File

@@ -3,7 +3,7 @@ import importlib
import logging
from abc import abstractmethod
from collections.abc import Callable
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@@ -239,7 +239,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
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)
current_profit = trade.calc_profit_ratio(current_rate)
if trade.is_short:

View File

@@ -5,7 +5,7 @@ import re
import shutil
import threading
import warnings
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any, TypedDict
@@ -116,7 +116,7 @@ class FreqaiDataDrawer:
if metric not in self.metric_tracker[pair]:
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]["timestamp"].append(timestamp)

View File

@@ -3,7 +3,7 @@ import inspect
import logging
import random
import shutil
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@@ -341,7 +341,7 @@ class FreqaiDataKitchen:
full_timerange = TimeRange.parse_timerange(tr)
config_timerange = TimeRange.parse_timerange(self.config["timerange"])
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_backtest = copy.deepcopy(full_timerange)
@@ -525,7 +525,7 @@ class FreqaiDataKitchen:
:return:
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
max_time = self.freqai_config.get("expiration_hours", 0)
if max_time > 0:
@@ -536,7 +536,7 @@ class FreqaiDataKitchen:
def check_if_new_training_required(
self, trained_timestamp: int
) -> tuple[bool, TimeRange, TimeRange]:
time = datetime.now(tz=timezone.utc).timestamp()
time = datetime.now(tz=UTC).timestamp()
trained_timerange = TimeRange()
data_load_timerange = TimeRange()

View File

@@ -3,7 +3,7 @@ import threading
import time
from abc import ABC, abstractmethod
from collections import deque
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Literal
@@ -76,7 +76,7 @@ class IFreqaiModel(ABC):
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config)
# 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.scanning = False
self.ft_params = self.freqai_info["feature_parameters"]

View File

@@ -1,5 +1,5 @@
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
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
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")

View File

@@ -5,7 +5,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
import logging
import traceback
from copy import deepcopy
from datetime import datetime, time, timedelta, timezone
from datetime import UTC, datetime, time, timedelta
from math import isclose
from threading import Lock
from time import sleep
@@ -93,14 +93,16 @@ class FreqtradeBot(LoggingMixin):
# Remove credentials from original exchange config to avoid accidental credential exposure
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)
# Check config consistency here since strategies can set certain options
validate_config_consistency(config)
self.exchange = ExchangeResolver.load_exchange(
self.config, exchange_config=exchange_config, load_leverage_tiers=True
)
# Re-validate exchange compatibility
self.exchange.validate_config(self.config)
init_db(self.config["db_url"])
@@ -266,7 +268,7 @@ class FreqtradeBot(LoggingMixin):
)
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:
@@ -296,7 +298,7 @@ class FreqtradeBot(LoggingMixin):
self._schedule.run_pending()
Trade.commit()
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:
"""
@@ -421,7 +423,7 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException as 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(
"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}")
stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
trade=trade,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
current_rate=current_entry_rate,
current_profit=current_entry_profit,
min_stake=min_entry_stake,
@@ -916,7 +918,7 @@ class FreqtradeBot(LoggingMixin):
amount=amount,
rate=enter_limit_requested,
time_in_force=time_in_force,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
entry_tag=enter_tag,
side=trade_side,
):
@@ -987,7 +989,7 @@ class FreqtradeBot(LoggingMixin):
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker="maker")
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(
pair=pair,
@@ -1106,7 +1108,7 @@ class FreqtradeBot(LoggingMixin):
)(
pair=pair,
trade=trade,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
proposed_rate=enter_limit_requested,
entry_tag=entry_tag,
side=trade_side,
@@ -1124,7 +1126,7 @@ class FreqtradeBot(LoggingMixin):
else:
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
current_rate=enter_limit_requested,
proposed_leverage=1.0,
max_leverage=max_leverage,
@@ -1157,7 +1159,7 @@ class FreqtradeBot(LoggingMixin):
self.strategy.custom_stake_amount, default_retval=stake_amount
)(
pair=pair,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
current_rate=enter_limit_requested,
proposed_stake=stake_amount,
min_stake=min_stake_amount,
@@ -1214,6 +1216,7 @@ class FreqtradeBot(LoggingMixin):
"leverage": trade.leverage if trade.leverage else None,
"direction": "Short" if trade.is_short else "Long",
"limit": open_rate, # Deprecated (?)
"order_rate": open_rate,
"open_rate": open_rate,
"order_type": order_type or "unknown",
"stake_amount": stake_amount,
@@ -1222,7 +1225,7 @@ class FreqtradeBot(LoggingMixin):
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
"fiat_currency": self.config.get("fiat_display_currency", None),
"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,
"sub_trade": sub_trade,
}
@@ -1250,6 +1253,7 @@ class FreqtradeBot(LoggingMixin):
"leverage": trade.leverage,
"direction": "Short" if trade.is_short else "Long",
"limit": trade.open_rate,
"order_rate": trade.open_rate,
"order_type": order_type,
"stake_amount": trade.stake_amount,
"open_rate": trade.open_rate,
@@ -1361,7 +1365,7 @@ class FreqtradeBot(LoggingMixin):
exits: list[ExitCheckTuple] = self.strategy.should_exit(
trade,
exit_rate,
datetime.now(timezone.utc),
datetime.now(UTC),
enter=enter,
exit_=exit_,
force_stoploss=0,
@@ -1479,44 +1483,6 @@ class FreqtradeBot(LoggingMixin):
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]):
"""
Perform required actions according to existing stoploss orders of trade
@@ -1558,6 +1524,44 @@ class FreqtradeBot(LoggingMixin):
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:
"""
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
@@ -1583,9 +1587,7 @@ class FreqtradeBot(LoggingMixin):
if not_closed:
if fully_cancelled or (
open_order
and self.strategy.ft_check_timed_out(
trade, open_order, datetime.now(timezone.utc)
)
and self.strategy.ft_check_timed_out(trade, open_order, datetime.now(UTC))
):
self.handle_cancel_order(
order, open_order, trade, constants.CANCEL_REASON["TIMEOUT"]
@@ -1683,7 +1685,7 @@ class FreqtradeBot(LoggingMixin):
trade=trade,
order=order_obj,
pair=trade.pair,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
proposed_rate=proposed_rate,
current_order_rate=order_obj.safe_placement_price,
entry_tag=trade.enter_tag,
@@ -2075,7 +2077,7 @@ class FreqtradeBot(LoggingMixin):
)(
pair=trade.pair,
trade=trade,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
proposed_rate=proposed_limit_rate,
current_profit=current_profit,
exit_tag=exit_reason,
@@ -2106,7 +2108,7 @@ class FreqtradeBot(LoggingMixin):
time_in_force=time_in_force,
exit_reason=exit_reason,
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}.")
@@ -2202,7 +2204,7 @@ class FreqtradeBot(LoggingMixin):
"enter_tag": trade.enter_tag,
"exit_reason": trade.exit_reason,
"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_currency": self.config["stake_currency"],
"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",
"gain": gain,
"limit": profit_rate or 0,
"order_rate": profit_rate or 0,
"order_type": order_type,
"amount": order.safe_amount_after_fee,
"open_rate": trade.open_rate,
@@ -2257,7 +2260,7 @@ class FreqtradeBot(LoggingMixin):
"enter_tag": trade.enter_tag,
"exit_reason": trade.exit_reason,
"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"],
"base_currency": self.exchange.get_pair_base_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:
if order.status in constants.NON_OPEN_EXCHANGE_STATES:
strategy_safe_wrapper(self.strategy.order_filled, default_retval=None)(
pair=trade.pair, trade=trade, order=order, current_time=datetime.now(timezone.utc)
strategy_safe_wrapper(self.strategy.order_filled, supress_error=True)(
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 order.ft_order_side == trade.entry_side:
@@ -2365,14 +2368,14 @@ class FreqtradeBot(LoggingMixin):
stake_currency=self.config["stake_currency"],
dry_run=self.config["dry_run"],
)
if self.strategy.use_custom_stoploss:
current_rate = self.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=True
)
profit = trade.calc_profit_ratio(current_rate)
self.strategy.ft_stoploss_adjust(
current_rate, trade, datetime.now(timezone.utc), profit, 0, after_fill=True
)
if self.strategy.use_custom_stoploss and trade.is_open:
current_rate = self.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=True
)
profit = trade.calc_profit_ratio(current_rate)
self.strategy.ft_stoploss_adjust(
current_rate, trade, datetime.now(UTC), profit, 0, after_fill=True
)
# Updating wallets when order is closed
self.wallets.update()
return trade
@@ -2397,7 +2400,7 @@ class FreqtradeBot(LoggingMixin):
def handle_protections(self, pair: str, side: LongShort) -> None:
# 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)
if prot_trig:
msg: RPCProtectionMsg = {

View File

@@ -8,4 +8,4 @@ from freqtrade.ft_types.backtest_result_type import (
get_BacktestResultType_default,
)
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

View File

@@ -1,8 +1,8 @@
from datetime import datetime
from typing import Literal
from typing import Literal, Required
from pydantic import TypeAdapter
from typing_extensions import Required, TypedDict
from typing_extensions import TypedDict
class AnnotationType(TypedDict, total=False):

View File

@@ -58,7 +58,7 @@ def setup_logging_pre() -> None:
FT_LOGGING_CONFIG = {
"version": 1,
# "incremental": True,
# "disable_existing_loggers": False,
"disable_existing_loggers": False,
"formatters": {
"basic": {"format": "%(message)s"},
"standard": {
@@ -223,7 +223,7 @@ def setup_logging(config: Config) -> None:
logger.info("Enabling colorized output.")
error_console._color_system = error_console._detect_color_system()
logging.info("Logfile configured")
logger.info("Logfile configured")
# Set verbosity levels
logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG)

View File

@@ -10,8 +10,8 @@ from typing import Any
# check min. python version
if sys.version_info < (3, 10): # pragma: no cover # noqa: UP036
sys.exit("Freqtrade requires Python version >= 3.10")
if sys.version_info < (3, 11): # pragma: no cover # noqa: UP036
sys.exit("Freqtrade requires Python version >= 3.11")
from freqtrade import __version__
from freqtrade.commands import Arguments

View File

@@ -125,6 +125,7 @@ class LookaheadAnalysis(BaseAnalysis):
backtesting = Backtesting(prepare_data_config, self.exchange)
self.exchange = backtesting.exchange
self.local_config["candle_type_def"] = prepare_data_config["candle_type_def"]
self._fee = backtesting.fee
backtesting._set_strategy(backtesting.strategylist[0])

View File

@@ -743,7 +743,7 @@ class Backtesting:
if order and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_date, trade)
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,
trade=trade, # type: ignore[arg-type]
order=order,

View File

@@ -1,6 +1,6 @@
import logging
from copy import deepcopy
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from pandas import DataFrame
@@ -38,7 +38,7 @@ class BaseAnalysis:
@staticmethod
def dt_to_timestamp(dt: datetime):
timestamp = int(dt.replace(tzinfo=timezone.utc).timestamp())
timestamp = int(dt.replace(tzinfo=UTC).timestamp())
return timestamp
def fill_full_varholder(self):
@@ -48,12 +48,12 @@ class BaseAnalysis:
parsed_timerange = TimeRange.parse_timerange(self.local_config["timerange"])
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:
self.full_varHolder.from_dt = parsed_timerange.startdt
if parsed_timerange.stopdt is None:
self.full_varHolder.to_dt = datetime.now(timezone.utc)
self.full_varHolder.to_dt = datetime.now(UTC)
else:
self.full_varHolder.to_dt = parsed_timerange.stopdt

View File

@@ -6,7 +6,7 @@ and will be sent to the hyperopt worker processes.
import logging
import sys
import warnings
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@@ -273,7 +273,7 @@ class HyperOptimizer:
Keep this function as optimized as possible!
"""
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
backtest_start_time = datetime.now(timezone.utc)
backtest_start_time = datetime.now(UTC)
# Apply parameters
if HyperoptTools.has_space(self.config, "buy"):
@@ -330,7 +330,7 @@ class HyperOptimizer:
bt_results = self.backtesting.backtest(
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(
{
"backtest_start_time": int(backtest_start_time.timestamp()),

View File

@@ -1,7 +1,7 @@
import logging
from collections.abc import Iterator
from copy import deepcopy
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@@ -71,7 +71,7 @@ class HyperoptTools:
"strategy_name": strategy_name,
"params": final_params,
"ft_stratparam_v": 1,
"export_time": datetime.now(timezone.utc),
"export_time": datetime.now(UTC),
}
logger.info(f"Dumping parameters to {filename}")
with filename.open("w") as f:

View File

@@ -332,8 +332,11 @@ def text_table_add_metrics(strat_results: dict) -> None:
),
),
(
"Avg. daily profit %",
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}",
"Avg. daily profit",
fmt_coin(
(strat_results["profit_total_abs"] / strat_results["backtest_days"]),
strat_results["stake_currency"],
),
),
(
"Avg. stake amount",

View File

@@ -1,6 +1,6 @@
import logging
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Any, Literal
import numpy as np
@@ -83,7 +83,6 @@ def _generate_result_line(
"""
Generate one result dict, with "first_column" as key.
"""
profit_sum = result["profit_ratio"].sum()
# (end-capital - starting capital) / starting capital
profit_total = result["profit_abs"].sum() / starting_balance
backtest_days = (max_date - min_date).days or 1
@@ -108,8 +107,6 @@ def _generate_result_line(
"profit_mean_pct": (
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": profit_total,
"profit_total_pct": round(profit_total * 100.0, 2),
@@ -518,14 +515,16 @@ def generate_strategy_stats(
best_pair = (
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
else None
)
worst_pair = (
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
else None
@@ -652,9 +651,9 @@ def generate_strategy_stats(
"max_drawdown_abs": 0.0,
"max_drawdown_low": 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_end": datetime(1970, 1, 1, tzinfo=timezone.utc),
"drawdown_end": datetime(1970, 1, 1, tzinfo=UTC),
"drawdown_end_ts": 0,
"csum_min": 0,
"csum_max": 0,

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from enum import Enum
from typing import ClassVar, Literal
@@ -114,7 +114,7 @@ class KeyValueStore:
if kv.value_type == ValueTypesEnum.STRING:
return kv.string_value
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:
return kv.float_value
if kv.value_type == ValueTypesEnum.INT:
@@ -156,7 +156,7 @@ class KeyValueStore:
)
if kv is None or kv.datetime_value is None:
return None
return kv.datetime_value.replace(tzinfo=timezone.utc)
return kv.datetime_value.replace(tzinfo=UTC)
@staticmethod
def get_float_value(key: KeyStoreKeys) -> float | None:
@@ -207,5 +207,5 @@ def set_startup_time() -> None:
if t is not None:
KeyValueStore.store_value("bot_start_time", t.open_date_utc)
else:
KeyValueStore.store_value("bot_start_time", datetime.now(timezone.utc))
KeyValueStore.store_value("startup_time", datetime.now(timezone.utc))
KeyValueStore.store_value("bot_start_time", datetime.now(UTC))
KeyValueStore.store_value("startup_time", datetime.now(UTC))

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any, ClassVar
from sqlalchemy import ScalarResult, String, or_, select
@@ -69,11 +69,9 @@ class PairLock(ModelBase):
"id": self.id,
"pair": self.pair,
"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_timestamp": int(
self.lock_end_time.replace(tzinfo=timezone.utc).timestamp() * 1000
),
"lock_end_timestamp": int(self.lock_end_time.replace(tzinfo=UTC).timestamp() * 1000),
"reason": self.reason,
"side": self.side,
"active": self.active,

View File

@@ -1,6 +1,6 @@
import logging
from collections.abc import Sequence
from datetime import datetime, timezone
from datetime import UTC, datetime
from sqlalchemy import select
@@ -52,7 +52,7 @@ class PairLocks:
"""
lock = PairLock(
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),
reason=reason,
side=side,
@@ -77,7 +77,7 @@ class PairLocks:
:param side: Side get locks for, can be 'long', 'short', '*' or None
"""
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
if PairLocks.use_db:
return PairLock.query_pair_locks(pair, now, side).all()
@@ -114,7 +114,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
logger.info(f"Releasing all locks for {pair}.")
locks = PairLocks.get_pair_locks(pair, now, side=side)
@@ -132,7 +132,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
if PairLocks.use_db:
# used in live modes
@@ -161,7 +161,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
return len(PairLocks.get_pair_locks("*", now, side)) > 0
@@ -173,7 +173,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc)
"""
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(
now, side

View File

@@ -6,9 +6,9 @@ import logging
from collections import defaultdict
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime, timezone
from datetime import UTC, datetime
from math import isclose
from typing import Any, ClassVar, Optional, cast
from typing import Any, ClassVar, Optional, Self, cast
from sqlalchemy import (
Enum,
@@ -25,7 +25,6 @@ from sqlalchemy import (
select,
)
from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship, validates
from typing_extensions import Self
from freqtrade.constants import (
CANCELED_EXCHANGE_STATES,
@@ -121,14 +120,12 @@ class Order(ModelBase):
@property
def order_date_utc(self) -> datetime:
"""Order-date with UTC timezoneinfo"""
return self.order_date.replace(tzinfo=timezone.utc)
return self.order_date.replace(tzinfo=UTC)
@property
def order_filled_utc(self) -> datetime | None:
"""last order-date with UTC timezoneinfo"""
return (
self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None
)
return self.order_filled_date.replace(tzinfo=UTC) if self.order_filled_date else None
@property
def safe_amount(self) -> float:
@@ -229,7 +226,7 @@ class Order(ModelBase):
self.order_filled_date = dt_from_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]:
order: dict[str, Any] = {
@@ -286,7 +283,7 @@ class Order(ModelBase):
self.order_date.strftime(DATETIME_PRINT_FORMAT) if self.order_date else None
),
"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
else None
),
@@ -533,7 +530,7 @@ class LocalTrade:
@property
def open_date_utc(self):
return self.open_date.replace(tzinfo=timezone.utc)
return self.open_date.replace(tzinfo=UTC)
@property
def stoploss_last_update_utc(self):
@@ -543,7 +540,7 @@ class LocalTrade:
@property
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
def entry_side(self) -> str:
@@ -1056,7 +1053,7 @@ class LocalTrade:
return zero
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)
total_seconds = FtPrecise((now - open_date).total_seconds())
hours = total_seconds / sec_per_hour or zero
@@ -1572,12 +1569,12 @@ class LocalTrade:
fee_close=data["fee_close"],
fee_close_cost=data.get("fee_close_cost"),
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_requested=data.get("open_rate_requested", data["open_rate"]),
open_trade_value=data.get("open_trade_value"),
close_date=(
datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc)
datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=UTC)
if data["close_timestamp"]
else None
),
@@ -1622,7 +1619,7 @@ class LocalTrade:
if order.get("order_date")
else None,
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"]
else None
),
@@ -2093,32 +2090,34 @@ class Trade(ModelBase, LocalTrade):
return resp
@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.
NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum)
"""
filters: list = [Trade.is_open.is_(False)]
if start_date:
filters.append(Trade.close_date >= start_date)
if not trade_filter:
trade_filter = []
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()
# returns pair, profit_ratio, abs_profit, count
return best_pair
@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
NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum)
"""
filters = [Order.status == "closed"]
if start_date:
filters.append(Order.order_filled_date >= start_date)
if not trade_filter:
trade_filter = []
trade_filter.append(Order.status == "closed")
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()
return trading_volume or 0.0

View File

@@ -1,5 +1,5 @@
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
import pandas as pd
@@ -638,7 +638,7 @@ def load_and_plot_trades(config: Config):
exchange = ExchangeResolver.load_exchange(config)
IStrategy.dp = DataProvider(config, exchange)
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)
timerange = plot_elements["timerange"]
trades = plot_elements["trades"]

View File

@@ -3,7 +3,7 @@ Protection manager class
"""
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
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:
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
result = None
for protection_handler in self._protection_handlers:
if protection_handler.has_global_stop:
@@ -65,7 +65,7 @@ class ProtectionManager:
self, pair, now: datetime | None = None, side: LongShort = "long"
) -> PairLock | None:
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
result = None
for protection_handler in self._protection_handlers:
if protection_handler.has_local_stop:

View File

@@ -1,7 +1,7 @@
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Any
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])
# coming from Database, tzinfo is not set.
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:
# unlock_at case with fixed hour of the day

View File

@@ -54,7 +54,7 @@ class StrategyResolver(IResolver):
strategy.ft_load_params_from_file()
# Set attributes
# Check if we need to override configuration
# (Attribute name, default, subkey)
# (Attribute name, default, subkey)
attributes = [
("minimal_roi", {"0": 10.0}),
("timeframe", None),

View File

@@ -1,6 +1,6 @@
import logging
import secrets
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Any
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
to_encode = data.copy()
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
expire = datetime.now(timezone.utc) + timedelta(days=30)
expire = datetime.now(UTC) + timedelta(days=30)
else:
raise ValueError()
to_encode.update(
{
"exp": expire,
"iat": datetime.now(timezone.utc),
"iat": datetime.now(UTC),
"type": token_type,
}
)

View File

@@ -163,11 +163,22 @@ class Profit(BaseModel):
max_drawdown_start_timestamp: int
max_drawdown_end: str
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
bot_start_timestamp: int
bot_start_date: str
class ProfitAll(BaseModel):
all: Profit
long: Profit | None = None
short: Profit | None = None
class SellReason(BaseModel):
wins: int
losses: int

View File

@@ -43,6 +43,7 @@ from freqtrade.rpc.api_server.api_schemas import (
Ping,
PlotConfig,
Profit,
ProfitAll,
ResultMsg,
ShowConfig,
Stats,
@@ -89,7 +90,8 @@ logger = logging.getLogger(__name__)
# 2.40: Add hyperopt-loss endpoint
# 2.41: Add download-data endpoint
# 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.
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"))
@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"])
def stats(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_stats()

View File

@@ -1,7 +1,7 @@
from typing import Any, Literal
from typing import Any, Literal, NotRequired
from uuid import uuid4
from typing_extensions import NotRequired, TypedDict
from typing_extensions import TypedDict
from freqtrade.exchange.exchange import Exchange

View File

@@ -102,7 +102,7 @@ class WebSocketChannel:
self._send_times.append(total_time)
self._calc_send_limit()
except asyncio.TimeoutError:
except TimeoutError:
logger.info(f"Connection for {self} timed out, disconnecting")
raise
@@ -201,8 +201,8 @@ class WebSocketChannel:
try:
await task
except (
TimeoutError,
asyncio.CancelledError,
asyncio.TimeoutError,
WebSocketDisconnect,
ConnectionClosed,
RuntimeError,

View File

@@ -266,7 +266,7 @@ class ExternalMessageConsumer:
except Exception as 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.
try:
# ping

View File

@@ -5,7 +5,7 @@ This module contains class to define a RPC communications
import logging
from abc import abstractmethod
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
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.ft_types import AnnotationType
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.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@@ -375,7 +375,7 @@ class RPC:
"""
:param timeunit: Valid entries are 'days', 'weeks', 'months'
"""
start_date = datetime.now(timezone.utc).date()
start_date = datetime.now(UTC).date()
if timeunit == "weeks":
# weekly
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}
return {"exit_reasons": exit_reasons, "durations": durations}
def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str, start_date: datetime | None = None
def _collect_trade_statistics_data(
self,
trades: Sequence["Trade"],
stake_currency: str,
fiat_display_currency: str,
) -> dict[str, Any]:
"""Returns cumulative profit statistics"""
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()
"""Iterate trades, calculate various statistics, and return intermediate results."""
profit_all_coin = []
profit_all_ratio = []
profit_closed_coin = []
@@ -544,7 +537,7 @@ class RPC:
losing_trades += 1
losing_profit += profit_abs
else:
# Get current rate
# Get current rate for open trades
if len(trade.select_filled_orders(trade.entry_side)) == 0:
# Skip trades with no filled orders
continue
@@ -558,17 +551,74 @@ class RPC:
profit_abs = nan
else:
_profit = trade.calculate_profit(trade.close_rate or current_rate)
profit_ratio = _profit.profit_ratio
profit_abs = _profit.total_profit
profit_all_coin.append(profit_abs)
profit_all_ratio.append(profit_ratio)
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])
best_pair = Trade.get_best_pair(start_date)
trading_volume = Trade.get_trading_volume(start_date)
best_pair_filters = [Trade.close_date > 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
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),
"drawdown_high": drawdown.high_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,
"bot_start_timestamp": dt_ts_def(bot_start, 0),
"bot_start_date": format_date(bot_start),
@@ -1094,7 +1149,7 @@ class RPC:
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
if not trade:
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
for open_order in trade.open_orders:
@@ -1115,13 +1170,16 @@ class RPC:
c_count += 1
except ExchangeError:
pass
trade_pair = trade.pair
trade.delete()
self._freqtrade.wallets.update()
return {
"result": "success",
"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,
}
@@ -1259,7 +1317,7 @@ class RPC:
for lock in locks:
lock.active = False
lock.lock_end_time = datetime.now(timezone.utc)
lock.lock_end_time = datetime.now(UTC)
Trade.commit()

View File

@@ -56,7 +56,8 @@ class __RPCEntryExitMsgBase(RPCSendMsgBase):
quote_currency: str
leverage: float | None
direction: str
limit: float
limit: float # Deprecated, use order_rate instead
order_rate: float
open_rate: float
order_type: str
stake_amount: float
@@ -87,7 +88,6 @@ class RPCExitMsg(__RPCEntryExitMsgBase):
exit_reason: str | None
close_date: datetime
# current_rate: float | None
order_rate: float | None
final_profit_ratio: float | None
is_final_exit: bool

View File

@@ -191,8 +191,8 @@ class Telegram(RPCHandler):
r"/mix_tags",
r"/daily$",
r"/daily \d+$",
r"/profit$",
r"/profit \d+",
r"/profit([_ ]long|[_ ]short)?$",
r"/profit([_ ]long|[_ ]short)? \d+$",
r"/stats$",
r"/count$",
r"/locks$",
@@ -305,13 +305,17 @@ class Telegram(RPCHandler):
CommandHandler("order", self._order),
CommandHandler("list_custom_data", self._list_custom_data),
CommandHandler("tg_info", self._tg_info),
CommandHandler("profit_long", self._profit_long),
CommandHandler("profit_short", self._profit_short),
]
callbacks = [
CallbackQueryHandler(self._status_table, pattern="update_status_table"),
CallbackQueryHandler(self._daily, pattern="update_daily"),
CallbackQueryHandler(self._weekly, pattern="update_weekly"),
CallbackQueryHandler(self._monthly, pattern="update_monthly"),
CallbackQueryHandler(self._profit, pattern="update_profit"),
CallbackQueryHandler(self._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._performance, pattern="update_performance"),
CallbackQueryHandler(
@@ -995,29 +999,25 @@ class Telegram(RPCHandler):
"""
await self._timeunit_stats(update, context, "months")
@authorized_only
async def _profit(self, update: Update, context: CallbackContext) -> None:
def _format_profit_message(
self,
stats: dict,
stake_cur: str,
fiat_disp_cur: str,
timescale: int | None = None,
direction: str | None = None,
) -> str:
"""
Handler for /profit.
Returns a cumulative profit statistics.
:param bot: telegram bot
:param update: message update
:return: None
Format profit statistics message for telegram.
:param stats: Trade statistics dictionary
:param stake_cur: Stake currency
: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"]
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)
# Extract common variables
profit_closed_coin = stats["profit_closed_coin"]
profit_closed_ratio_mean = stats["profit_closed_ratio_mean"]
profit_closed_percent = stats["profit_closed_percent"]
@@ -1037,62 +1037,153 @@ class Telegram(RPCHandler):
expectancy = stats["expectancy"]
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:
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:
# Message to display
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 = (
"*ROI:* Closed trades\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:
markdown_msg = "`No closed trade` \n"
fiat_all_trades = (
f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
)
markdown_msg = no_closed_msg
fiat_all_trades = (
f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
)
markdown_msg += (
f"{all_roi_label}\n"
f"∙ `{fmt_coin(profit_all_coin, stake_cur)} "
f"({profit_all_ratio_mean:.2%}) "
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_all_trades}"
f"*Total Trade Count:* `{trade_count}`\n"
f"*Bot started:* `{stats['bot_start_date']}`\n"
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
f"`{first_trade_date}`\n"
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 += (
f"*ROI:* All trades\n"
f"∙ `{fmt_coin(profit_all_coin, stake_cur)} "
f"({profit_all_ratio_mean:.2%}) "
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_all_trades}"
f"*Total Trade Count:* `{trade_count}`\n"
f"*Bot started:* `{stats['bot_start_date']}`\n"
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
f"`{first_trade_date}`\n"
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})`"
f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} "
f"({best_pair_profit_ratio:.2%})`\n"
f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n"
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n"
f" from `{stats['max_drawdown_start']} "
f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n"
f" to `{stats['max_drawdown_end']} "
f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n"
f"*Current Drawdown:* `{stats['current_drawdown']:.2%} "
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 += (
f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} "
f"({best_pair_profit_ratio:.2%})`\n"
f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n"
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n"
f" from `{stats['max_drawdown_start']} "
f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n"
f" to `{stats['max_drawdown_end']} "
f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n"
)
return markdown_msg
async def _profit_handler(
self,
update: Update,
context: CallbackContext,
direction: str | None = None,
) -> None:
"""
Common handler for profit commands.
: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(
markdown_msg,
reload_able=True,
callback_path="update_profit",
callback_path="update_profit" if not direction else f"update_profit_{direction}",
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
async def _stats(self, update: Update, context: CallbackContext) -> None:
"""
@@ -1484,7 +1575,7 @@ class Telegram(RPCHandler):
trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id)
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."
)
@@ -1865,6 +1956,10 @@ class Telegram(RPCHandler):
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
"over the last n days`\n"
"*/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"
"*/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"

View File

@@ -5,7 +5,7 @@ This module defines the interface to apply for strategies
import logging
from abc import ABC, abstractmethod
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from math import isinf, isnan
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.
: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:
"""
@@ -1158,7 +1158,7 @@ class IStrategy(ABC, HyperStrategyMixin):
manually from within the strategy, to allow an easy way to unlock pairs.
: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(
self, pair: str, *, candle_date: datetime | None = None, side: str = "*"

View File

@@ -39,6 +39,17 @@ class StrategyUpdater:
"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
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):
# if the name is in the mapping, update it
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
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
def visit_ImportFrom(self, node):
# if hasattr(node, "module"):
# if node.module == "freqtrade.strategy.hyper":
# node.module = "freqtrade.strategy"
if node.module in StrategyUpdater.module_replacements:
mod = node.module
StrategyUpdater.module_replacements[node.module]["aliases"].add(mod)
return node
def visit_If(self, node: ast_comments.If):
@@ -182,6 +201,12 @@ class NameUpdater(ast_comments.NodeTransformer):
and node.attr == "nr_of_successful_buys"
):
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
def visit_ClassDef(self, node):

View File

@@ -1,5 +1,5 @@
import re
from datetime import datetime, timezone
from datetime import UTC, datetime
from time import time
import humanize
@@ -9,7 +9,7 @@ from freqtrade.constants import DATETIME_PRINT_FORMAT
def dt_now() -> datetime:
"""Return the current datetime in UTC."""
return datetime.now(timezone.utc)
return datetime.now(UTC)
def dt_utc(
@@ -22,7 +22,7 @@ def dt_utc(
microsecond: int = 0,
) -> datetime:
"""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:
@@ -68,7 +68,7 @@ def dt_from_ts(timestamp: float) -> datetime:
if timestamp > 1e10:
# Timezone in ms - convert to seconds
timestamp /= 1000
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
return datetime.fromtimestamp(timestamp, tz=UTC)
def shorten_date(_date: str) -> str:

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):
return _start_cap
else:
return _start_cap.get("stake_currency")
return _start_cap.get(config["stake_currency"], 0.0)

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from cachetools import TTLCache
@@ -11,7 +11,7 @@ class PeriodicCache(TTLCache):
def __init__(self, maxsize, ttl, getsizeof=None):
def local_timer():
ts = datetime.now(timezone.utc).timestamp()
ts = datetime.now(UTC).timestamp()
offset = ts % ttl
return ts - offset

View File

@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2025.6"
__version__ = "2025.7"
if "dev" in __version__:
from pathlib import Path

View File

@@ -13,14 +13,13 @@ authors = [
description = "Freqtrade - Client scripts"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
license = {text = "GPLv3"}
# license = "GPLv3"
classifiers = [
"Environment :: Console",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",

View File

@@ -1,3 +1,3 @@
# Requirements for freqtrade client library
requests==2.32.4
python-rapidjson==1.20
python-rapidjson==1.21

View File

@@ -13,13 +13,12 @@ authors = [
description = "Freqtrade - Crypto Trading Bot"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
license = {text = "GPLv3"}
classifiers = [
"Environment :: Console",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
@@ -111,6 +110,7 @@ develop = [
"pytest-xdist",
"pytest",
"ruff",
"scipy-stubs",
"time-machine",
"types-cachetools",
"types-filelock",

View File

@@ -6,16 +6,16 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.12.1
mypy==1.16.1
ruff==0.12.5
mypy==1.17.0
pre-commit==4.2.0
pytest==8.4.1
pytest-asyncio==1.0.0
pytest-asyncio==1.1.0
pytest-cov==6.2.1
pytest-mock==3.14.1
pytest-random-order==1.2.0
pytest-timeout==2.4.0
pytest-xdist==3.7.0
pytest-xdist==3.8.0
isort==6.0.1
# For datetime mocking
time-machine==2.16.0
@@ -24,8 +24,9 @@ time-machine==2.16.0
nbconvert==7.16.6
# 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-requests==2.32.4.20250611
types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20250516
types-python-dateutil==2.9.0.20250708

View File

@@ -5,7 +5,7 @@
torch==2.7.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==0.29.1
# 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
# Progress bar for stable-baselines3 and sb3-contrib
tqdm==4.67.1

View File

@@ -3,10 +3,10 @@
-r requirements-plot.txt
# Required for freqai
scikit-learn==1.7.0
scikit-learn==1.7.1
joblib==1.5.1
catboost==1.2.8; 'arm' not in platform_machine
lightgbm==4.6.0
xgboost==3.0.2
tensorboard==2.19.0
tensorboard==2.20.0
datasieve==0.1.9

View File

@@ -2,8 +2,8 @@
-r requirements.txt
# Required for hyperopt
scipy==1.15.3
scikit-learn==1.7.0
scipy==1.16.1
scikit-learn==1.7.1
filelock==3.18.0
optuna==4.4.0
cmaes==0.11.1
cmaes==0.12.0

View File

@@ -1,42 +1,42 @@
numpy==2.2.6
pandas==2.3.0
numpy==2.3.2
pandas==2.3.1
bottleneck==1.5.0
numexpr==2.11.0
# Indicator libraries
ft-pandas-ta==0.3.15
ta-lib==0.5.5
technical==1.5.1
technical==1.5.2
ccxt==4.4.91
cryptography==45.0.4
aiohttp==3.12.13
ccxt==4.4.96
cryptography==45.0.5
aiohttp==3.12.14
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 ~
httpx>=0.24.1
humanize==4.12.3
cachetools==6.1.0
requests==2.32.4
urllib3==2.5.0
certifi==2025.6.15
jsonschema==4.24.0
certifi==2025.7.14
jsonschema==4.25.0
tabulate==0.9.0
pycoingecko==3.2.0
jinja2==3.1.6
joblib==1.5.1
rich==14.0.0
pyarrow==20.0.0; platform_machine != 'armv7l'
rich==14.1.0
pyarrow==21.0.0; platform_machine != 'armv7l'
# Load ticker files 30% faster
python-rapidjson==1.20
python-rapidjson==1.21
# Properly format api responses
orjson==3.10.18
orjson==3.11.1
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.115.14
fastapi==0.116.1
pydantic==2.11.7
uvicorn==0.35.0
pyjwt==2.10.1

View File

@@ -234,7 +234,7 @@ async def create_client(
await protocol.on_message(ws, name, message)
except (asyncio.TimeoutError, websockets.exceptions.WebSocketException):
except (TimeoutError, websockets.exceptions.WebSocketException):
# Try pinging
try:
pong = await ws.ping()
@@ -244,7 +244,7 @@ async def create_client(
continue
except asyncio.TimeoutError:
except TimeoutError:
logger.error(f"Ping timed out, retrying in {sleep_time}s")
await asyncio.sleep(sleep_time)

View File

@@ -153,16 +153,13 @@ function Find-PythonExecutable {
"python3.13",
"python3.12",
"python3.11",
"python3.10",
"python3",
"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\Python311\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe",
"C:\Python313\python.exe",
"C:\Python312\python.exe",
"C:\Python311\python.exe",
"C:\Python310\python.exe"
"C:\Python311\python.exe"
)
@@ -178,10 +175,10 @@ function Main {
"Starting the operations..." | 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
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
}

View File

@@ -25,7 +25,7 @@ function check_installed_python() {
exit 2
fi
for v in 13 12 11 10
for v in 13 12 11
do
PYTHON="python3.${v}"
which $PYTHON
@@ -36,7 +36,7 @@ function check_installed_python() {
fi
done
echo "No usable python found. Please make sure to have python3.10 or newer installed."
echo "No usable python found. Please make sure to have python3.11 or newer installed."
exit 1
}
@@ -257,7 +257,7 @@ function install() {
install_redhat
else
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."
sleep 10
fi
@@ -284,7 +284,7 @@ function help() {
echo " -p,--plot Install dependencies for Plotting scripts."
}
# Verify if 3.10+ is installed
# Verify if 3.11+ is installed
check_installed_python
case $* in

View File

@@ -4,7 +4,7 @@ import logging
import platform
import re
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
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):
np.random.seed(42)
if not start_date:
start_date = datetime(2020, 1, 1, tzinfo=timezone.utc)
start_date = datetime(2020, 1, 1, tzinfo=UTC)
# Generate random data
end_date = start_date + timedelta(days=days)
@@ -258,6 +258,7 @@ def patch_exchange(
"._supported_trading_mode_margin_pairs",
PropertyMock(
return_value=[
(TradingMode.SPOT, MarginMode.NONE),
(TradingMode.MARGIN, MarginMode.CROSS),
(TradingMode.MARGIN, MarginMode.ISOLATED),
(TradingMode.FUTURES, MarginMode.CROSS),
@@ -3405,4 +3406,35 @@ def leverage_tiers():
"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,
},
],
}

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
@@ -43,7 +43,7 @@ def mock_trade_1(fee, is_short: bool):
fee_open=fee.return_value,
fee_close=fee.return_value,
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,
exchange="binance",
strategy="StrategyTestV3",
@@ -106,8 +106,8 @@ def mock_trade_2(fee, is_short: bool):
timeframe=5,
enter_tag="TEST1",
exit_reason="sell_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=UTC) - timedelta(minutes=2),
is_short=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",
timeframe=5,
exit_reason="roi",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc),
open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=UTC),
is_short=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,
fee_open=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,
open_rate=0.123,
exchange="binance",
@@ -260,7 +260,7 @@ def mock_trade_5(fee, is_short: bool):
amount_requested=124.0,
fee_open=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,
open_rate=0.123,
exchange="binance",
@@ -316,7 +316,7 @@ def mock_trade_6(fee, is_short: bool):
stake_amount=0.001,
amount=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_close=fee.return_value,
is_open=True,
@@ -410,7 +410,7 @@ def short_trade(fee):
strategy="DefaultStrategy",
timeframe=5,
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),
is_short=True,
)
@@ -500,8 +500,8 @@ def leverage_trade(fee):
strategy="DefaultStrategy",
timeframe=5,
exit_reason="sell_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=300),
close_date=datetime.now(tz=timezone.utc),
open_date=datetime.now(tz=UTC) - timedelta(minutes=300),
close_date=datetime.now(tz=UTC),
interest_rate=0.0005,
)
o = Order.parse_from_ccxt_object(leverage_order(), "DOGE/BTC", "sell")

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
@@ -55,8 +55,8 @@ def mock_trade_usdt_1(fee, is_short: bool):
stake_amount=20.0,
amount=2.0,
amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=5),
open_date=datetime.now(tz=UTC) - timedelta(days=2, minutes=20),
close_date=datetime.now(tz=UTC) - timedelta(days=2, minutes=5),
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=False,
@@ -127,8 +127,8 @@ def mock_trade_usdt_2(fee, is_short: bool):
timeframe=5,
enter_tag="TEST1",
exit_reason="exit_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=UTC) - timedelta(minutes=2),
is_short=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,
enter_tag="TEST3",
exit_reason="roi",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc),
open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=UTC),
is_short=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,
fee_open=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,
open_rate=2.0,
exchange="binance",
@@ -280,7 +280,7 @@ def mock_trade_usdt_5(fee, is_short: bool):
amount_requested=10.01,
fee_open=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,
open_rate=2.0,
exchange="binance",
@@ -332,7 +332,7 @@ def mock_trade_usdt_6(fee, is_short: bool):
stake_amount=20.0,
amount=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_close=fee.return_value,
is_open=True,
@@ -374,7 +374,7 @@ def mock_trade_usdt_7(fee, is_short: bool):
fee_open=fee.return_value,
fee_close=fee.return_value,
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,
exchange="binance",
strategy="StrategyTestV2",

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock
from zipfile import ZipFile
@@ -182,19 +182,19 @@ def test_extract_trades_of_period(testdatadir):
"profit_abs": [0.0, 1, -2, -5],
"open_date": to_datetime(
[
datetime(2017, 11, 13, 15, 40, 0, tzinfo=timezone.utc),
datetime(2017, 11, 14, 9, 41, 0, tzinfo=timezone.utc),
datetime(2017, 11, 14, 14, 20, 0, tzinfo=timezone.utc),
datetime(2017, 11, 15, 3, 40, 0, tzinfo=timezone.utc),
datetime(2017, 11, 13, 15, 40, 0, tzinfo=UTC),
datetime(2017, 11, 14, 9, 41, 0, tzinfo=UTC),
datetime(2017, 11, 14, 14, 20, 0, tzinfo=UTC),
datetime(2017, 11, 15, 3, 40, 0, tzinfo=UTC),
],
utc=True,
),
"close_date": to_datetime(
[
datetime(2017, 11, 13, 16, 40, 0, tzinfo=timezone.utc),
datetime(2017, 11, 14, 10, 41, 0, tzinfo=timezone.utc),
datetime(2017, 11, 14, 15, 25, 0, tzinfo=timezone.utc),
datetime(2017, 11, 15, 3, 55, 0, tzinfo=timezone.utc),
datetime(2017, 11, 13, 16, 40, 0, tzinfo=UTC),
datetime(2017, 11, 14, 10, 41, 0, tzinfo=UTC),
datetime(2017, 11, 14, 15, 25, 0, tzinfo=UTC),
datetime(2017, 11, 15, 3, 55, 0, tzinfo=UTC),
],
utc=True,
),
@@ -203,10 +203,10 @@ def test_extract_trades_of_period(testdatadir):
trades1 = extract_trades_of_period(data, trades)
# First and last trade are dropped as they are out of range
assert len(trades1) == 2
assert trades1.iloc[0].open_date == datetime(2017, 11, 14, 9, 41, 0, tzinfo=timezone.utc)
assert trades1.iloc[0].close_date == datetime(2017, 11, 14, 10, 41, 0, tzinfo=timezone.utc)
assert trades1.iloc[-1].open_date == datetime(2017, 11, 14, 14, 20, 0, tzinfo=timezone.utc)
assert trades1.iloc[-1].close_date == datetime(2017, 11, 14, 15, 25, 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=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=UTC)
def test_analyze_trade_parallelism(testdatadir):
@@ -293,7 +293,7 @@ def test_combined_dataframes_with_rel_mean(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
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 "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, 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))]
df = DataFrame(zip(profits, dates, strict=False), columns=["profit_abs", "open_date"])
# sort by profit and reset index

View File

@@ -1,7 +1,7 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103
import re
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from unittest.mock import MagicMock
@@ -165,19 +165,19 @@ def test_datahandler_ohlcv_data_min_max(testdatadir):
# Empty pair
min_max = dh.ohlcv_data_min_max("UNITTEST/BTC", "8m", "spot")
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]
# Empty pair2
min_max = dh.ohlcv_data_min_max("NOPAIR/XXX", "41m", "spot")
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]
# Existing pair ...
min_max = dh.ohlcv_data_min_max("UNITTEST/BTC", "1m", "spot")
assert len(min_max) == 3
assert min_max[0] == datetime(2017, 11, 4, 23, 2, tzinfo=timezone.utc)
assert min_max[1] == datetime(2017, 11, 14, 22, 59, 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=UTC)
def test_datahandler__check_empty_df(testdatadir, caplog):
@@ -467,14 +467,14 @@ def test_datahandler_trades_data_min_max(testdatadir):
# Empty pair
min_max = dh.trades_data_min_max("NADA/ETH", TradingMode.SPOT)
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]
# Existing pair ...
min_max = dh.trades_data_min_max("XRP/ETH", TradingMode.SPOT)
assert len(min_max) == 3
assert min_max[0] == datetime(2019, 10, 11, 0, 0, 11, 620000, tzinfo=timezone.utc)
assert min_max[1] == datetime(2019, 10, 13, 11, 19, 28, 844000, 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=UTC)
def test_gethandlerclass():

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