mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-12-02 18:13:04 +00:00
Merge pull request #12052 from freqtrade/new_release
New release 2025.7
This commit is contained in:
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
|
||||
|
||||
|
||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/docker-build.yml
vendored
8
.github/workflows/docker-build.yml
vendored
@@ -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
29
.github/workflows/zizmor.yml
vendored
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
18
Dockerfile
18
Dockerfile
@@ -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
|
||||
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2025.6"
|
||||
__version__ = "2025.7"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
__main__.py for Freqtrade
|
||||
To launch Freqtrade as a module
|
||||
|
||||
> python -m freqtrade (with Python >= 3.10)
|
||||
> python -m freqtrade (with Python >= 3.11)
|
||||
"""
|
||||
|
||||
from freqtrade import main
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {}"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
24
freqtrade/exchange/luno.py
Normal file
24
freqtrade/exchange/luno.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import logging
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Luno(Exchange):
|
||||
"""
|
||||
Luno exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
|
||||
Please note that this exchange is not included in the list of exchanges
|
||||
officially supported by the Freqtrade development team. So some features
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_has_history": False, # Only provides the last 1000 candles
|
||||
"always_require_api_keys": True, # Requires API keys to fetch candles
|
||||
"trades_has_history": False, # Only the last 24h are available
|
||||
}
|
||||
27
freqtrade/exchange/modetrade.py
Normal file
27
freqtrade/exchange/modetrade.py
Normal 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),
|
||||
# ]
|
||||
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = "*"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Requirements for freqtrade client library
|
||||
requests==2.32.4
|
||||
python-rapidjson==1.20
|
||||
python-rapidjson==1.21
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
8
setup.sh
8
setup.sh
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user