Merge branch 'develop' into pr/gaardiolor/10839

This commit is contained in:
Matthias
2024-11-12 18:05:25 +01:00
166 changed files with 4555 additions and 2369 deletions

View File

@@ -46,7 +46,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip key: pip-${{ matrix.python-version }}-ubuntu
- name: TA binary *nix - name: TA binary *nix
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
@@ -167,7 +167,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/Library/Caches/pip path: ~/Library/Caches/pip
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip key: pip-${{ matrix.os }}-${{ matrix.python-version }}
- name: TA binary *nix - name: TA binary *nix
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
@@ -196,7 +196,7 @@ jobs:
rm /usr/local/bin/python3.11-config || true rm /usr/local/bin/python3.11-config || true
rm /usr/local/bin/python3.12-config || true rm /usr/local/bin/python3.12-config || true
brew install hdf5 c-blosc libomp brew install libomp
- name: Installation (python) - name: Installation (python)
run: | run: |
@@ -280,7 +280,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~\AppData\Local\pip\Cache path: ~\AppData\Local\pip\Cache
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip key: pip-${{ matrix.os }}-${{ matrix.python-version }}
- name: Installation - name: Installation
run: | run: |
@@ -420,7 +420,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip key: pip-3.12-ubuntu
- name: TA binary *nix - name: TA binary *nix
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
@@ -540,12 +540,12 @@ jobs:
- name: Publish to PyPI (Test) - name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.10.3 uses: pypa/gh-action-pypi-publish@v1.12.2
with: with:
repository-url: https://test.pypi.org/legacy/ repository-url: https://test.pypi.org/legacy/
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.10.3 uses: pypa/gh-action-pypi-publish@v1.12.2
deploy-docker: deploy-docker:

View File

@@ -9,7 +9,7 @@ repos:
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.12.1" rev: "v1.13.0"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
@@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.7.0' rev: 'v0.7.3'
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format - id: ruff-format

View File

@@ -682,12 +682,18 @@
}, },
"exit_fill": { "exit_fill": {
"description": "Telegram setting for exit fill signals.", "description": "Telegram setting for exit fill signals.",
"type": "string", "type": [
"enum": [ "string",
"on", "object"
"off",
"silent"
], ],
"additionalProperties": {
"type": "string",
"enum": [
"on",
"off",
"silent"
]
},
"default": "on" "default": "on"
}, },
"exit_cancel": { "exit_cancel": {
@@ -1383,6 +1389,11 @@
"type": "string", "type": "string",
"default": "example" "default": "example"
}, },
"wait_for_training_iteration_on_reload": {
"description": "Wait for the next training iteration to complete after /reload or ctrl+c.",
"type": "boolean",
"default": true
},
"feature_parameters": { "feature_parameters": {
"description": "The parameters used to engineer the feature set", "description": "The parameters used to engineer the feature set",
"type": "object", "type": "object",

View File

@@ -37,8 +37,8 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
min_date: datetime, min_date: datetime,
max_date: datetime, max_date: datetime,
config: Config, config: Config,
processed: Dict[str, DataFrame], processed: dict[str, DataFrame],
backtest_stats: Dict[str, Any], backtest_stats: dict[str, Any],
**kwargs, **kwargs,
) -> float: ) -> float:
""" """
@@ -103,7 +103,7 @@ class MyAwesomeStrategy(IStrategy):
SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'), SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'),
] ]
def generate_roi_table(params: Dict) -> Dict[int, float]: def generate_roi_table(params: Dict) -> dict[int, float]:
roi_table = {} roi_table = {}
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']

View File

@@ -10,12 +10,14 @@ To learn how to get data for the pairs and exchange you're interested in, head o
``` ```
usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [-s NAME] [-d PATH] [--userdir PATH] [-s NAME]
[--strategy-path PATH] [-i TIMEFRAME] [--strategy-path PATH]
[--timerange TIMERANGE] [--recursive-strategy-search]
[--data-format-ohlcv {json,jsongz,hdf5}] [--freqaimodel NAME] [--freqaimodel-path PATH]
[-i TIMEFRAME] [--timerange TIMERANGE]
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
[--max-open-trades INT] [--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[-p PAIRS [PAIRS ...]] [--eps] [--dmmp] [-p PAIRS [PAIRS ...]] [--eps]
[--enable-protections] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET] [--dry-run-wallet DRY_RUN_WALLET]
[--timeframe-detail TIMEFRAME_DETAIL] [--timeframe-detail TIMEFRAME_DETAIL]
@@ -24,8 +26,9 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--export-filename PATH] [--export-filename PATH]
[--breakdown {day,week,month} [{day,week,month} ...]] [--breakdown {day,week,month} [{day,week,month} ...]]
[--cache {none,day,week,month}] [--cache {none,day,week,month}]
[--freqai-backtest-live-models]
optional arguments: options:
-h, --help show this help message and exit -h, --help show this help message and exit
-i TIMEFRAME, --timeframe TIMEFRAME -i TIMEFRAME, --timeframe TIMEFRAME
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
@@ -48,10 +51,6 @@ optional arguments:
--eps, --enable-position-stacking --eps, --enable-position-stacking
Allow buying the same pair multiple times (position Allow buying the same pair multiple times (position
stacking). stacking).
--dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high
number).
--enable-protections, --enableprotections --enable-protections, --enableprotections
Enable protections for backtesting.Will slow Enable protections for backtesting.Will slow
backtesting down by a considerable amount, but will backtesting down by a considerable amount, but will
@@ -80,10 +79,13 @@ optional arguments:
--cache {none,day,week,month} --cache {none,day,week,month}
Load a cached backtest result no older than specified Load a cached backtest result no older than specified
age (default: day). age (default: day).
--freqai-backtest-live-models
Run backtest with ready models.
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are: --logfile FILE, --log-file FILE
Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more 'syslog', 'journald'. See the documentation for more
details. details.
-V, --version show program's version number and exit -V, --version show program's version number and exit
@@ -92,7 +94,7 @@ Common arguments:
`userdir/config.json` or `config.json` whichever `userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin. set to `-` to read config from stdin.
-d PATH, --datadir PATH -d PATH, --datadir PATH, --data-dir PATH
Path to directory with historical backtesting data. Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH --userdir PATH, --user-data-dir PATH
Path to userdata directory. Path to userdata directory.
@@ -102,6 +104,12 @@ Strategy arguments:
Specify strategy class name which will be used by the Specify strategy class name which will be used by the
bot. bot.
--strategy-path PATH Specify additional strategy lookup path. --strategy-path PATH Specify additional strategy lookup path.
--recursive-strategy-search
Recursively search for a strategy in the strategies
folder.
--freqaimodel NAME Specify a custom freqaimodels.
--freqaimodel-path PATH
Specify additional lookup path for freqaimodels.
``` ```
@@ -558,6 +566,7 @@ Since backtesting lacks some detailed information about what happens within a ca
- Stoploss - Stoploss
- ROI - ROI
- Trailing stoploss - Trailing stoploss
- Position reversals (futures only) happen if an entry signal in the other direction than the closing trade triggers at the candle the existing trade closes.
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode. Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
Also, keep in mind that past results don't guarantee future success. Also, keep in mind that past results don't guarantee future success.

View File

@@ -252,6 +252,14 @@ OKX requires a passphrase for each api key, you will therefore need to add this
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0). Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value. The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
Gate API keys require the following permissions on top of the market type you want to trade:
* "Spot Trade" _or_ "Perpetual Futures" (Read and Write) (either select both, or the one matching the market you want to trade)
* "Wallet" (read only)
* "Account" (read only)
Without these permissions, the bot will not start correctly and show errors like "permission missing".
## Bybit ## Bybit
Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode. Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode.

View File

@@ -293,10 +293,10 @@ class MyCoolPyTorchClassifier(BasePyTorchClassifier):
super().__init__(**kwargs) super().__init__(**kwargs)
config = self.freqai_info.get("model_training_parameters", {}) config = self.freqai_info.get("model_training_parameters", {})
self.learning_rate: float = config.get("learning_rate", 3e-4) self.learning_rate: float = config.get("learning_rate", 3e-4)
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) self.model_kwargs: dict[str, Any] = config.get("model_kwargs", {})
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) self.trainer_kwargs: dict[str, Any] = config.get("trainer_kwargs", {})
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
""" """
User sets up the training and test data to fit their desired model here User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary holding all data for train, test, :param data_dictionary: the dictionary holding all data for train, test,
@@ -359,10 +359,10 @@ class PyTorchMLPRegressor(BasePyTorchRegressor):
super().__init__(**kwargs) super().__init__(**kwargs)
config = self.freqai_info.get("model_training_parameters", {}) config = self.freqai_info.get("model_training_parameters", {})
self.learning_rate: float = config.get("learning_rate", 3e-4) self.learning_rate: float = config.get("learning_rate", 3e-4)
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) self.model_kwargs: dict[str, Any] = config.get("model_kwargs", {})
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) self.trainer_kwargs: dict[str, Any] = config.get("trainer_kwargs", {})
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
n_features = data_dictionary["train_features"].shape[-1] n_features = data_dictionary["train_features"].shape[-1]
model = PyTorchMLPModel( model = PyTorchMLPModel(
input_dim=n_features, input_dim=n_features,

View File

@@ -22,6 +22,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False` | `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
| `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer. | `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer.
| `activate_tensorboard` | <br> Indicate whether or not to activate tensorboard for the tensorboard enabled modules (currently Reinforcment Learning, XGBoost, Catboost, and PyTorch). Tensorboard needs Torch installed, which means you will need the torch/RL docker image or you need to answer "yes" to the install question about whether or not you wish to install Torch. <br> **Datatype:** Boolean. <br> Default: `True`. | `activate_tensorboard` | <br> Indicate whether or not to activate tensorboard for the tensorboard enabled modules (currently Reinforcment Learning, XGBoost, Catboost, and PyTorch). Tensorboard needs Torch installed, which means you will need the torch/RL docker image or you need to answer "yes" to the install question about whether or not you wish to install Torch. <br> **Datatype:** Boolean. <br> Default: `True`.
| `wait_for_training_iteration_on_reload` | <br> When using /reload or ctrl-c, wait for the current training iteration to finish before completing graceful shutdown. If set to `False`, FreqAI will break the current training iteration, allowing you to shutdown gracefully more quickly, but you will lose your current training iteration. <br> **Datatype:** Boolean. <br> Default: `True`.
### Feature parameters ### Feature parameters

View File

@@ -42,11 +42,11 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--recursive-strategy-search] [--freqaimodel NAME] [--recursive-strategy-search] [--freqaimodel NAME]
[--freqaimodel-path PATH] [-i TIMEFRAME] [--freqaimodel-path PATH] [-i TIMEFRAME]
[--timerange TIMERANGE] [--timerange TIMERANGE]
[--data-format-ohlcv {json,jsongz,hdf5}] [--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
[--max-open-trades INT] [--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[-p PAIRS [PAIRS ...]] [--hyperopt-path PATH] [-p PAIRS [PAIRS ...]] [--hyperopt-path PATH]
[--eps] [--dmmp] [--enable-protections] [--eps] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET] [--dry-run-wallet DRY_RUN_WALLET]
[--timeframe-detail TIMEFRAME_DETAIL] [-e INT] [--timeframe-detail TIMEFRAME_DETAIL] [-e INT]
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]] [--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]]
@@ -55,15 +55,15 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--hyperopt-loss NAME] [--disable-param-export] [--hyperopt-loss NAME] [--disable-param-export]
[--ignore-missing-spaces] [--analyze-per-epoch] [--ignore-missing-spaces] [--analyze-per-epoch]
optional arguments: options:
-h, --help show this help message and exit -h, --help show this help message and exit
-i TIMEFRAME, --timeframe TIMEFRAME -i TIMEFRAME, --timeframe TIMEFRAME
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
--data-format-ohlcv {json,jsongz,hdf5} --data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
Storage format for downloaded candle (OHLCV) data. Storage format for downloaded candle (OHLCV) data.
(default: `json`). (default: `feather`).
--max-open-trades INT --max-open-trades INT
Override the value of the `max_open_trades` Override the value of the `max_open_trades`
configuration setting. configuration setting.
@@ -80,10 +80,6 @@ optional arguments:
--eps, --enable-position-stacking --eps, --enable-position-stacking
Allow buying the same pair multiple times (position Allow buying the same pair multiple times (position
stacking). stacking).
--dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high
number).
--enable-protections, --enableprotections --enable-protections, --enableprotections
Enable protections for backtesting.Will slow Enable protections for backtesting.Will slow
backtesting down by a considerable amount, but will backtesting down by a considerable amount, but will
@@ -133,7 +129,8 @@ optional arguments:
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are: --logfile FILE, --log-file FILE
Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more 'syslog', 'journald'. See the documentation for more
details. details.
-V, --version show program's version number and exit -V, --version show program's version number and exit
@@ -142,7 +139,7 @@ Common arguments:
`userdir/config.json` or `config.json` whichever `userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin. set to `-` to read config from stdin.
-d PATH, --datadir PATH -d PATH, --datadir PATH, --data-dir PATH
Path to directory with historical backtesting data. Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH --userdir PATH, --user-data-dir PATH
Path to userdata directory. Path to userdata directory.
@@ -867,18 +864,15 @@ You can use the `--print-all` command line option if you would like to see all r
## Position stacking and disabling max market positions ## Position stacking and disabling max market positions
In some situations, you may need to run Hyperopt (and Backtesting) with the In some situations, you may need to run Hyperopt (and Backtesting) with the `--eps`/`--enable-position-staking` argument, or you may need to set `max_open_trades` to a very high number to disable the limit on the number of open trades.
`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments.
By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one
open trade is allowed for every traded pair. The total number of trades open for all pairs open trade per pair is allowed. The total number of trades open for all pairs
is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to
some potential trades to be hidden (or masked) by previously open trades. potential trades being hidden (or masked) by already open trades.
The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times.
while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` Using `--max-open-trades` with a very high number will disable the limit on the number of open trades.
during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high
number).
!!! Note !!! Note
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.
@@ -923,7 +917,7 @@ After you run Hyperopt for the desired amount of epochs, you can later list all
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt for Backtesting.
### Why do my backtest results not match my hyperopt results? ### Why do my backtest results not match my hyperopt results?
Should results not match, check the following factors: Should results not match, check the following factors:

View File

@@ -352,7 +352,7 @@ The optional `bearer_token` will be included in the requests Authorization Heade
#### MarketCapPairList #### MarketCapPairList
`MarketCapPairList` employs sorting/filtering of pairs by their marketcap rank based of CoinGecko. It will only recognize coins up to the coin placed at rank 250. The returned pairlist will be sorted based of their marketcap ranks. `MarketCapPairList` employs sorting/filtering of pairs by their marketcap rank based of CoinGecko. The returned pairlist will be sorted based of their marketcap ranks.
```json ```json
"pairlists": [ "pairlists": [
@@ -367,6 +367,7 @@ The optional `bearer_token` will be included in the requests Authorization Heade
``` ```
`number_assets` defines the maximum number of pairs returned by the pairlist. `max_rank` will determine the maximum rank used in creating/filtering the pairlist. It's expected that some coins within the top `max_rank` marketcap will not be included in the resulting pairlist since not all pairs will have active trading pairs in your preferred market/stake/exchange combination. `number_assets` defines the maximum number of pairs returned by the pairlist. `max_rank` will determine the maximum rank used in creating/filtering the pairlist. It's expected that some coins within the top `max_rank` marketcap will not be included in the resulting pairlist since not all pairs will have active trading pairs in your preferred market/stake/exchange combination.
While using a `max_rank` bigger than 250 is supported, it's not recommended, as it'll cause multiple API calls to CoinGecko, which can lead to rate limit issues.
The `refresh_period` setting defines the interval (in seconds) at which the marketcap rank data will be refreshed. The default is 86,400 seconds (1 day). The pairlist cache (`refresh_period`) applies to both generating pairlists (when in the first position in the list) and filtering instances (when not in the first position in the list). The `refresh_period` setting defines the interval (in seconds) at which the marketcap rank data will be refreshed. The default is 86,400 seconds (1 day). The pairlist cache (`refresh_period`) applies to both generating pairlists (when in the first position in the list) and filtering instances (when not in the first position in the list).

View File

@@ -67,6 +67,18 @@ OS Specific steps are listed first, the common section below is necessary for al
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git curl sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git curl
``` ```
=== "MacOS"
#### Install necessary dependencies
Install [Homebrew](https://brew.sh/) if you don't have it already.
```bash
# install packages
brew install gettext libomp
```
!!! Note
The `setup.sh` script will install these dependencies for you - assuming brew is installed on your system.
=== "RaspberryPi/Raspbian" === "RaspberryPi/Raspbian"
The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/). The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/).
This image comes with python3.11 preinstalled, making it easy to get freqtrade up and running. This image comes with python3.11 preinstalled, making it easy to get freqtrade up and running.
@@ -76,7 +88,7 @@ OS Specific steps are listed first, the common section below is necessary for al
```bash ```bash
sudo apt-get install python3-venv libatlas-base-dev cmake curl sudo apt-get install python3-venv libatlas-base-dev cmake curl
# Use pywheels.org to speed up installation # Use piwheels.org to speed up installation
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
git clone https://github.com/freqtrade/freqtrade.git git clone https://github.com/freqtrade/freqtrade.git
@@ -150,9 +162,7 @@ Each time you open a new terminal, you must run `source .venv/bin/activate` to a
source ./.venv/bin/activate source ./.venv/bin/activate
``` ```
### Congratulations [You are now ready](#you-are-ready) to run the bot.
[You are ready](#you-are-ready), and run the bot
### Other options of /setup.sh script ### Other options of /setup.sh script
@@ -220,7 +230,7 @@ cd ..
rm -rf ./ta-lib* rm -rf ./ta-lib*
``` ```
#### Setup Python virtual environment (virtualenv) ### Setup Python virtual environment (virtualenv)
You will run freqtrade in separated `virtual environment` You will run freqtrade in separated `virtual environment`
@@ -232,19 +242,18 @@ python3 -m venv .venv
source .venv/bin/activate source .venv/bin/activate
``` ```
#### Install python dependencies ### Install python dependencies
```bash ```bash
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txt python3 -m pip install -r requirements.txt
# install freqtrade
python3 -m pip install -e . python3 -m pip install -e .
``` ```
### Congratulations [You are now ready](#you-are-ready) to run the bot.
[You are ready](#you-are-ready), and run the bot ### (Optional) Post-installation Tasks
#### (Optional) Post-installation Tasks
!!! Note !!! Note
If you run the bot on a server, you should consider using [Docker](docker_quickstart.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. If you run the bot on a server, you should consider using [Docker](docker_quickstart.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
@@ -333,9 +342,7 @@ cd build_helpers
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
``` ```
### Congratulations [You are now ready](#you-are-ready) to run the bot.
[You are ready](#you-are-ready), and run the bot
### Important shortcuts ### Important shortcuts

View File

@@ -1,7 +1,7 @@
markdown==3.7 markdown==3.7
mkdocs==1.6.1 mkdocs==1.6.1
mkdocs-material==9.5.42 mkdocs-material==9.5.44
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.11.2 pymdown-extensions==10.12
jinja2==3.1.4 jinja2==3.1.4
mike==2.1.3 mike==2.1.3

View File

@@ -166,6 +166,7 @@ Called for open trade every iteration (roughly every 5 seconds) until a trade is
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object. The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade), and is still mandatory. The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade), and is still mandatory.
As custom stoploss acts as regular, changing stoploss, it will behave similar to `trailing_stop` - and trades exiting due to this will have the exit_reason of `"trailing_stop_loss"`.
The method must return a stoploss value (float / number) as a percentage of the current price. The method must return a stoploss value (float / number) as a percentage of the current price.
E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD. E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
@@ -834,7 +835,7 @@ class DigDeeperStrategy(IStrategy):
current_entry_rate: float, current_exit_rate: float, current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float, current_entry_profit: float, current_exit_profit: float,
**kwargs **kwargs
) -> Union[Optional[float], Tuple[Optional[float], Optional[str]]]: ) -> Union[Optional[float], tuple[Optional[float], Optional[str]]]:
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be Custom trade adjustment logic, returning the stake amount that a trade should be
increased or decreased. increased or decreased.
@@ -890,7 +891,7 @@ class DigDeeperStrategy(IStrategy):
# Hope you have a deep wallet! # Hope you have a deep wallet!
try: try:
# This returns first order stake size # This returns first order stake size
stake_amount = filled_entries[0].stake_amount stake_amount = filled_entries[0].stake_amount_filled
# This then calculates current safety order size # This then calculates current safety order size
stake_amount = stake_amount * (1 + (count_of_entries * 0.25)) stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
return stake_amount, "1/3rd_increase" return stake_amount, "1/3rd_increase"
@@ -975,7 +976,7 @@ class AwesomeStrategy(IStrategy):
pair == "BTC/USDT" pair == "BTC/USDT"
and entry_tag == "long_sma200" and entry_tag == "long_sma200"
and side == "long" and side == "long"
and (current_time - timedelta(minutes=10)) > trade.open_date_utc and (current_time - timedelta(minutes=10)) <= trade.open_date_utc
): ):
# just cancel the order if it has been filled more than half of the amount # just cancel the order if it has been filled more than half of the amount
if order.filled > order.remaining: if order.filled > order.remaining:

View File

@@ -4,7 +4,7 @@ This page explains how to customize your strategies, add new indicators and set
If you haven't already, please familiarize yourself with: If you haven't already, please familiarize yourself with:
- the [Freqtrade strategy 101](freqtrade-101.md), which provides a quick start to strategy development - the [Freqtrade strategy 101](strategy-101.md), which provides a quick start to strategy development
- the [Freqtrade bot basics](bot-basics.md), which provides overall info on how the bot operates - the [Freqtrade bot basics](bot-basics.md), which provides overall info on how the bot operates
## Develop your own strategy ## Develop your own strategy

View File

@@ -215,7 +215,7 @@ trades.groupby("pair")["exit_reason"].value_counts()
``` ```
## Analyze the loaded trades for trade parallelism ## Analyze the loaded trades for trade parallelism
This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`. This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with a very high `max_open_trades` setting.
`analyze_trade_parallelism()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle. `analyze_trade_parallelism()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle.

View File

@@ -780,7 +780,7 @@ class MyCoolFreqaiModel(BaseRegressionModel):
def predict( def predict(
self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
) -> Tuple[DataFrame, npt.NDArray[np.int_]]: ) -> tuple[DataFrame, npt.NDArray[np.int_]]:
# ... your custom stuff # ... your custom stuff

View File

@@ -58,6 +58,7 @@ For the Freqtrade configuration, you can then use the full value (including `-`
```json ```json
"chat_id": "-1001332619709" "chat_id": "-1001332619709"
``` ```
!!! Warning "Using telegram groups" !!! Warning "Using telegram groups"
When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasant surprises. When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasant surprises.
@@ -93,9 +94,12 @@ Example configuration showing the different settings:
"trailing_stop_loss": "on", "trailing_stop_loss": "on",
"stop_loss": "on", "stop_loss": "on",
"stoploss_on_exchange": "on", "stoploss_on_exchange": "on",
"custom_exit": "silent", "custom_exit": "silent", // custom_exit without specifying an exit reason
"partial_exit": "on" "partial_exit": "on",
// "custom_exit_message": "silent", // Disable individual custom exit reasons
"*": "off" // Disable all other exit reasons
}, },
// "exit": "off", // Simplistic configuration to disable all exit messages
"exit_cancel": "on", "exit_cancel": "on",
"exit_fill": "off", "exit_fill": "off",
"protection_trigger": "off", "protection_trigger": "off",
@@ -108,16 +112,16 @@ Example configuration showing the different settings:
}, },
``` ```
`entry` notifications are sent when the order is placed, while `entry_fill` notifications are sent when the order is filled on the exchange. * `entry` notifications are sent when the order is placed, while `entry_fill` notifications are sent when the order is filled on the exchange.
`exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange. * `exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
`*_fill` notifications are off by default and must be explicitly enabled. Exit messages (`exit` and `exit_fill`) can be further controlled at individual exit reasons level, with the specific exit reason as the key. the default for all exit reasons is `on` - but can be configured via special `*` key - which will act as a wildcard for all exit reasons that are not explicitly defined.
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered. * `*_fill` notifications are off by default and must be explicitly enabled.
`strategy_msg` - Receive notifications from the strategy, sent via `self.dp.send_msg()` from the strategy [more details](strategy-customization.md#send-notification). * `protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
`show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`. * `strategy_msg` - Receive notifications from the strategy, sent via `self.dp.send_msg()` from the strategy [more details](strategy-customization.md#send-notification).
* `show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`.
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. * `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
`allow_custom_messages` completely disable strategy messages. * `allow_custom_messages` completely disable strategy messages.
`reload` allows you to disable reload-buttons on selected messages. * `reload` allows you to disable reload-buttons on selected messages.
## Create a custom keyboard (command shortcut buttons) ## Create a custom keyboard (command shortcut buttons)
@@ -296,12 +300,12 @@ Return a summary of your profit/loss and performance.
The relative profit of `1.2%` is the average profit per trade. The relative profit of `1.2%` is the average profit per trade.
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`. The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. **Starting capital(**) is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. **Profit Factor** is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
Expectancy corresponds to the average return per currency unit at risk, i.e. the winrate and the risk-reward ratio (the average gain of winning trades compared to the average loss of losing trades). **Expectancy** corresponds to the average return per currency unit at risk, i.e. the winrate and the risk-reward ratio (the average gain of winning trades compared to the average loss of losing trades).
Expectancy Ratio is expected profit or loss of a subsequent trade based on the performance of all past trades. **Expectancy Ratio** is expected profit or loss of a subsequent trade based on the performance of all past trades.
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. **Max drawdown** corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date. **Bot started date** will refer to the date the bot was first started. For older bots, this will default to the first trade's open date.
### /forceexit <trade_id> ### /forceexit <trade_id>
@@ -345,7 +349,7 @@ Return the balance of all crypto-currency your have on the exchange.
> **Available:** 3.05890234 > **Available:** 3.05890234
> **Balance:** 3.05890234 > **Balance:** 3.05890234
> **Pending:** 0.0 > **Pending:** 0.0
>
> **Currency:** CVC > **Currency:** CVC
> **Available:** 86.64180098 > **Available:** 86.64180098
> **Balance:** 86.64180098 > **Balance:** 86.64180098
@@ -356,6 +360,7 @@ Return the balance of all crypto-currency your have on the exchange.
Per default `/daily` will return the 7 last days. The example below if for `/daily 3`: Per default `/daily` will return the 7 last days. The example below if for `/daily 3`:
> **Daily Profit over the last 3 days:** > **Daily Profit over the last 3 days:**
``` ```
Day (count) USDT USD Profit % Day (count) USDT USD Profit %
-------------- ------------ ---------- ---------- -------------- ------------ ---------- ----------
@@ -370,6 +375,7 @@ Per default `/weekly` will return the 8 last weeks, including the current week.
from Monday. The example below if for `/weekly 3`: from Monday. The example below if for `/weekly 3`:
> **Weekly Profit over the last 3 weeks (starting from Monday):** > **Weekly Profit over the last 3 weeks (starting from Monday):**
``` ```
Monday (count) Profit BTC Profit USD Profit % Monday (count) Profit BTC Profit USD Profit %
------------- -------------- ------------ ---------- ------------- -------------- ------------ ----------

View File

@@ -143,6 +143,7 @@ Most properties here can be None as they are dependent on the exchange response.
| `remaining` | float | Remaining amount | | `remaining` | float | Remaining amount |
| `cost` | float | Cost of the order - usually average * filled (*Exchange dependent on futures, may contain the cost with or without leverage and may be in contracts.*) | | `cost` | float | Cost of the order - usually average * filled (*Exchange dependent on futures, may contain the cost with or without leverage and may be in contracts.*) |
| `stake_amount` | float | Stake amount used for this order. *Added in 2023.7.* | | `stake_amount` | float | Stake amount used for this order. *Added in 2023.7.* |
| `stake_amount_filled` | float | Filled Stake amount used for this order. *Added in 2024.11.* |
| `order_date` | datetime | Order creation date **use `order_date_utc` instead** | | `order_date` | datetime | Order creation date **use `order_date_utc` instead** |
| `order_date_utc` | datetime | Order creation date (in UTC) | | `order_date_utc` | datetime | Order creation date (in UTC) |
| `order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead** | | `order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead** |

View File

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

View File

@@ -5,7 +5,7 @@ This module contains the argument manager class
from argparse import ArgumentParser, Namespace, _ArgumentGroup from argparse import ArgumentParser, Namespace, _ArgumentGroup
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Union from typing import Any
from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS
from freqtrade.constants import DEFAULT_CONFIG from freqtrade.constants import DEFAULT_CONFIG
@@ -37,7 +37,6 @@ ARGS_COMMON_OPTIMIZE = [
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + [ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + [
"position_stacking", "position_stacking",
"use_max_market_positions",
"enable_protections", "enable_protections",
"dry_run_wallet", "dry_run_wallet",
"timeframe_detail", "timeframe_detail",
@@ -53,7 +52,6 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + [
"hyperopt", "hyperopt",
"hyperopt_path", "hyperopt_path",
"position_stacking", "position_stacking",
"use_max_market_positions",
"enable_protections", "enable_protections",
"dry_run_wallet", "dry_run_wallet",
"timeframe_detail", "timeframe_detail",
@@ -117,7 +115,7 @@ ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
ARGS_BUILD_CONFIG = ["config"] ARGS_BUILD_CONFIG = ["config"]
ARGS_SHOW_CONFIG = ["user_data_dir", "config", "show_sensitive"] ARGS_SHOW_CONFIG = ["user_data_dir", "config", "show_sensitive"]
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"] ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "strategy_path", "template"]
ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase", "exchange"] ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase", "exchange"]
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"] ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
@@ -242,8 +240,7 @@ ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_s
ARGS_LOOKAHEAD_ANALYSIS = [ ARGS_LOOKAHEAD_ANALYSIS = [
a a
for a in ARGS_BACKTEST for a in ARGS_BACKTEST
if a if a not in ("position_stacking", "backtest_cache", "backtest_breakdown")
not in ("position_stacking", "use_max_market_positions", "backtest_cache", "backtest_breakdown")
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"] ] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"] ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
@@ -278,9 +275,9 @@ class Arguments:
Arguments Class. Manage the arguments received by the cli Arguments Class. Manage the arguments received by the cli
""" """
def __init__(self, args: Optional[list[str]]) -> None: def __init__(self, args: list[str] | None) -> None:
self.args = args self.args = args
self._parsed_arg: Optional[Namespace] = None self._parsed_arg: Namespace | None = None
def get_parsed_arg(self) -> dict[str, Any]: def get_parsed_arg(self) -> dict[str, Any]:
""" """
@@ -322,9 +319,7 @@ class Arguments:
return parsed_arg return parsed_arg
def _build_args( def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None:
self, optionlist: list[str], parser: Union[ArgumentParser, _ArgumentGroup]
) -> None:
for val in optionlist: for val in optionlist:
opt = AVAILABLE_CLI_OPTIONS[val] opt = AVAILABLE_CLI_OPTIONS[val]
parser.add_argument(*opt.cli, dest=val, **opt.kwargs) parser.add_argument(*opt.cli, dest=val, **opt.kwargs)

View File

@@ -168,14 +168,6 @@ AVAILABLE_CLI_OPTIONS = {
action="store_true", action="store_true",
default=False, default=False,
), ),
"use_max_market_positions": Arg(
"--dmmp",
"--disable-max-market-positions",
help="Disable applying `max_open_trades` during backtest "
"(same as setting `max_open_trades` to a very high number).",
action="store_false",
default=True,
),
"backtest_show_pair_list": Arg( "backtest_show_pair_list": Arg(
"--show-pair-list", "--show-pair-list",
help="Show backtesting pairlist sorted by profit.", help="Show backtesting pairlist sorted by profit.",

View File

@@ -86,7 +86,14 @@ def start_new_strategy(args: dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if "strategy" in args and args["strategy"]: if "strategy" in args and args["strategy"]:
new_path = config["user_data_dir"] / USERPATH_STRATEGIES / (args["strategy"] + ".py") if "strategy_path" in args and args["strategy_path"]:
strategy_dir = Path(args["strategy_path"])
else:
strategy_dir = config["user_data_dir"] / USERPATH_STRATEGIES
if not strategy_dir.is_dir():
logger.info(f"Creating strategy directory {strategy_dir}")
strategy_dir.mkdir(parents=True)
new_path = strategy_dir / (args["strategy"] + ".py")
if new_path.exists(): if new_path.exists():
raise OperationalException( raise OperationalException(

View File

@@ -1,6 +1,5 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional
import requests import requests
@@ -24,7 +23,7 @@ def clean_ui_subdir(directory: Path):
p.rmdir() p.rmdir()
def read_ui_version(dest_folder: Path) -> Optional[str]: def read_ui_version(dest_folder: Path) -> str | None:
file = dest_folder / ".uiversion" file = dest_folder / ".uiversion"
if not file.is_file(): if not file.is_file():
return None return None
@@ -52,7 +51,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
f.write(version) f.write(version)
def get_ui_download_url(version: Optional[str] = None) -> tuple[str, str]: def get_ui_download_url(version: str | None = None) -> tuple[str, str]:
base_url = "https://api.github.com/repos/freqtrade/frequi/" base_url = "https://api.github.com/repos/freqtrade/frequi/"
# Get base UI Repo path # Get base UI Repo path

View File

@@ -1,7 +1,7 @@
import csv import csv
import logging import logging
import sys import sys
from typing import Any, Union from typing import Any
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.exceptions import ConfigurationError, OperationalException
@@ -87,7 +87,7 @@ def _print_objs_tabular(objs: list, print_colorized: bool) -> None:
from rich.text import Text from rich.text import Text
names = [s["name"] for s in objs] names = [s["name"] for s in objs]
objs_to_print: list[dict[str, Union[Text, str]]] = [ objs_to_print: list[dict[str, Text | str]] = [
{ {
"name": Text(s["name"] if s["name"] else "--"), "name": Text(s["name"] if s["name"] else "--"),
"location": s["location_rel"], "location": s["location_rel"],

View File

@@ -517,8 +517,11 @@ CONF_SCHEMA = {
}, },
"exit_fill": { "exit_fill": {
"description": "Telegram setting for exit fill signals.", "description": "Telegram setting for exit fill signals.",
"type": "string", "type": ["string", "object"],
"enum": TELEGRAM_SETTING_OPTIONS, "additionalProperties": {
"type": "string",
"enum": TELEGRAM_SETTING_OPTIONS,
},
"default": "on", "default": "on",
}, },
"exit_cancel": { "exit_cancel": {
@@ -995,6 +998,13 @@ CONF_SCHEMA = {
"type": "string", "type": "string",
"default": "example", "default": "example",
}, },
"wait_for_training_iteration_on_reload": {
"description": (
"Wait for the next training iteration to complete after /reload or ctrl+c."
),
"type": "boolean",
"default": True,
},
"feature_parameters": { "feature_parameters": {
"description": "The parameters used to engineer the feature set", "description": "The parameters used to engineer the feature set",
"type": "object", "type": "object",

View File

@@ -5,9 +5,10 @@ This module contains the configuration class
import ast import ast
import logging import logging
import warnings import warnings
from collections.abc import Callable
from copy import deepcopy from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Optional from typing import Any
from freqtrade import constants from freqtrade import constants
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
@@ -37,9 +38,9 @@ class Configuration:
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
""" """
def __init__(self, args: dict[str, Any], runmode: Optional[RunMode] = None) -> None: def __init__(self, args: dict[str, Any], runmode: RunMode | None = None) -> None:
self.args = args self.args = args
self.config: Optional[Config] = None self.config: Config | None = None
self.runmode = runmode self.runmode = runmode
def get_config(self) -> Config: def get_config(self) -> Config:
@@ -241,11 +242,7 @@ class Configuration:
logstring="Parameter --enable-protections detected, enabling Protections. ...", logstring="Parameter --enable-protections detected, enabling Protections. ...",
) )
if "use_max_market_positions" in self.args and not self.args["use_max_market_positions"]: if "max_open_trades" in self.args and self.args["max_open_trades"]:
config.update({"use_max_market_positions": False})
logger.info("Parameter --disable-max-market-positions detected ...")
logger.info("max_open_trades set to unlimited ...")
elif "max_open_trades" in self.args and self.args["max_open_trades"]:
config.update({"max_open_trades": self.args["max_open_trades"]}) config.update({"max_open_trades": self.args["max_open_trades"]})
logger.info( logger.info(
"Parameter --max-open-trades detected, overriding max_open_trades to: %s ...", "Parameter --max-open-trades detected, overriding max_open_trades to: %s ...",
@@ -455,8 +452,8 @@ class Configuration:
config: Config, config: Config,
argname: str, argname: str,
logstring: str, logstring: str,
logfun: Optional[Callable] = None, logfun: Callable | None = None,
deprecated_msg: Optional[str] = None, deprecated_msg: str | None = None,
) -> None: ) -> None:
""" """
:param config: Configuration dictionary :param config: Configuration dictionary

View File

@@ -3,7 +3,6 @@ Functions to handle deprecated settings
""" """
import logging import logging
from typing import Optional
from freqtrade.constants import Config from freqtrade.constants import Config
from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.exceptions import ConfigurationError, OperationalException
@@ -14,9 +13,9 @@ logger = logging.getLogger(__name__)
def check_conflicting_settings( def check_conflicting_settings(
config: Config, config: Config,
section_old: Optional[str], section_old: str | None,
name_old: str, name_old: str,
section_new: Optional[str], section_new: str | None,
name_new: str, name_new: str,
) -> None: ) -> None:
section_new_config = config.get(section_new, {}) if section_new else config section_new_config = config.get(section_new, {}) if section_new else config
@@ -34,7 +33,7 @@ def check_conflicting_settings(
def process_removed_setting( def process_removed_setting(
config: Config, section1: str, name1: str, section2: Optional[str], name2: str config: Config, section1: str, name1: str, section2: str | None, name2: str
) -> None: ) -> None:
""" """
:param section1: Removed section :param section1: Removed section
@@ -54,9 +53,9 @@ def process_removed_setting(
def process_deprecated_setting( def process_deprecated_setting(
config: Config, config: Config,
section_old: Optional[str], section_old: str | None,
name_old: str, name_old: str,
section_new: Optional[str], section_new: str | None,
name_new: str, name_new: str,
) -> None: ) -> None:
check_conflicting_settings(config, section_old, name_old, section_new, name_new) check_conflicting_settings(config, section_old, name_old, section_new, name_new)

View File

@@ -1,7 +1,6 @@
import logging import logging
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import Optional
from freqtrade.configuration.detect_environment import running_in_docker from freqtrade.configuration.detect_environment import running_in_docker
from freqtrade.constants import ( from freqtrade.constants import (
@@ -18,7 +17,7 @@ from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def create_datadir(config: Config, datadir: Optional[str] = None) -> Path: def create_datadir(config: Config, datadir: str | None = None) -> Path:
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data") folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
if not datadir: if not datadir:
# set datadir # set datadir

View File

@@ -7,7 +7,7 @@ import re
import sys import sys
from copy import deepcopy from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any
import rapidjson import rapidjson
@@ -78,7 +78,7 @@ def load_config_file(path: str) -> dict[str, Any]:
def load_from_files( def load_from_files(
files: list[str], base_path: Optional[Path] = None, level: int = 0 files: list[str], base_path: Path | None = None, level: int = 0
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
Recursively load configuration files if specified. Recursively load configuration files if specified.

View File

@@ -5,7 +5,6 @@ This module contains the argument manager class
import logging import logging
import re import re
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional
from typing_extensions import Self from typing_extensions import Self
@@ -25,24 +24,24 @@ class TimeRange:
def __init__( def __init__(
self, self,
starttype: Optional[str] = None, starttype: str | None = None,
stoptype: Optional[str] = None, stoptype: str | None = None,
startts: int = 0, startts: int = 0,
stopts: int = 0, stopts: int = 0,
): ):
self.starttype: Optional[str] = starttype self.starttype: str | None = starttype
self.stoptype: Optional[str] = stoptype self.stoptype: str | None = stoptype
self.startts: int = startts self.startts: int = startts
self.stopts: int = stopts self.stopts: int = stopts
@property @property
def startdt(self) -> Optional[datetime]: def startdt(self) -> datetime | None:
if self.startts: if self.startts:
return datetime.fromtimestamp(self.startts, tz=timezone.utc) return datetime.fromtimestamp(self.startts, tz=timezone.utc)
return None return None
@property @property
def stopdt(self) -> Optional[datetime]: def stopdt(self) -> datetime | None:
if self.stopts: if self.stopts:
return datetime.fromtimestamp(self.stopts, tz=timezone.utc) return datetime.fromtimestamp(self.stopts, tz=timezone.utc)
return None return None
@@ -120,7 +119,7 @@ class TimeRange:
self.starttype = "date" self.starttype = "date"
@classmethod @classmethod
def parse_timerange(cls, text: Optional[str]) -> Self: def parse_timerange(cls, text: str | None) -> Self:
""" """
Parse the value of the argument --timerange to determine what is the range desired Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange :param text: value from --timerange

View File

@@ -4,7 +4,7 @@
bot constants bot constants
""" """
from typing import Any, Literal, Optional from typing import Any, Literal
from freqtrade.enums import CandleType, PriceType from freqtrade.enums import CandleType, PriceType
@@ -38,6 +38,7 @@ HYPEROPT_LOSS_BUILTIN = [
"MaxDrawDownHyperOptLoss", "MaxDrawDownHyperOptLoss",
"MaxDrawDownRelativeHyperOptLoss", "MaxDrawDownRelativeHyperOptLoss",
"ProfitDrawDownHyperOptLoss", "ProfitDrawDownHyperOptLoss",
"MultiMetricHyperOptLoss",
] ]
AVAILABLE_PAIRLISTS = [ AVAILABLE_PAIRLISTS = [
"StaticPairList", "StaticPairList",
@@ -193,7 +194,7 @@ ListPairsWithTimeframes = list[PairWithTimeframe]
# Type for trades list # Type for trades list
TradeList = list[list] TradeList = list[list]
# ticks, pair, timeframe, CandleType # ticks, pair, timeframe, CandleType
TickWithTimeframe = tuple[str, str, CandleType, Optional[int], Optional[int]] TickWithTimeframe = tuple[str, str, CandleType, int | None, int | None]
ListTicksWithTimeframes = list[TickWithTimeframe] ListTicksWithTimeframes = list[TickWithTimeframe]
LongShort = Literal["long", "short"] LongShort = Literal["long", "short"]

View File

@@ -6,7 +6,7 @@ import logging
from copy import copy from copy import copy
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Literal, Optional, Union from typing import Any, Literal
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@@ -53,7 +53,7 @@ BT_DATA_COLUMNS = [
] ]
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: def get_latest_optimize_filename(directory: Path | str, variant: str) -> str:
""" """
Get latest backtest export based on '.last_result.json'. Get latest backtest export based on '.last_result.json'.
:param directory: Directory to search for last result :param directory: Directory to search for last result
@@ -84,7 +84,7 @@ def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> s
return data[f"latest_{variant}"] return data[f"latest_{variant}"]
def get_latest_backtest_filename(directory: Union[Path, str]) -> str: def get_latest_backtest_filename(directory: Path | str) -> str:
""" """
Get latest backtest export based on '.last_result.json'. Get latest backtest export based on '.last_result.json'.
:param directory: Directory to search for last result :param directory: Directory to search for last result
@@ -97,7 +97,7 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
return get_latest_optimize_filename(directory, "backtest") return get_latest_optimize_filename(directory, "backtest")
def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str: def get_latest_hyperopt_filename(directory: Path | str) -> str:
""" """
Get latest hyperopt export based on '.last_result.json'. Get latest hyperopt export based on '.last_result.json'.
:param directory: Directory to search for last result :param directory: Directory to search for last result
@@ -114,9 +114,7 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
return "hyperopt_results.pickle" return "hyperopt_results.pickle"
def get_latest_hyperopt_file( def get_latest_hyperopt_file(directory: Path | str, predef_filename: str | None = None) -> Path:
directory: Union[Path, str], predef_filename: Optional[str] = None
) -> Path:
""" """
Get latest hyperopt export based on '.last_result.json'. Get latest hyperopt export based on '.last_result.json'.
:param directory: Directory to search for last result :param directory: Directory to search for last result
@@ -137,7 +135,7 @@ def get_latest_hyperopt_file(
return directory / get_latest_hyperopt_filename(directory) return directory / get_latest_hyperopt_filename(directory)
def load_backtest_metadata(filename: Union[Path, str]) -> dict[str, Any]: def load_backtest_metadata(filename: Path | str) -> dict[str, Any]:
""" """
Read metadata dictionary from backtest results file without reading and deserializing entire Read metadata dictionary from backtest results file without reading and deserializing entire
file. file.
@@ -154,7 +152,7 @@ def load_backtest_metadata(filename: Union[Path, str]) -> dict[str, Any]:
raise OperationalException("Unexpected error while loading backtest metadata.") from e raise OperationalException("Unexpected error while loading backtest metadata.") from e
def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType: def load_backtest_stats(filename: Path | str) -> BacktestResultType:
""" """
Load backtest statistics file. Load backtest statistics file.
:param filename: pathlib.Path object, or string pointing to the file. :param filename: pathlib.Path object, or string pointing to the file.
@@ -276,7 +274,7 @@ def get_backtest_market_change(filename: Path, include_ts: bool = True) -> pd.Da
def find_existing_backtest_stats( def find_existing_backtest_stats(
dirname: Union[Path, str], run_ids: dict[str, str], min_backtest_date: Optional[datetime] = None dirname: Path | str, run_ids: dict[str, str], min_backtest_date: datetime | None = None
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
Find existing backtest stats that match specified run IDs and load them. Find existing backtest stats that match specified run IDs and load them.
@@ -345,7 +343,7 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
return df return df
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame: def load_backtest_data(filename: Path | str, strategy: str | None = None) -> pd.DataFrame:
""" """
Load backtest data file. Load backtest data file.
:param filename: pathlib.Path object, or string pointing to a file or directory :param filename: pathlib.Path object, or string pointing to a file or directory
@@ -439,7 +437,7 @@ def evaluate_result_multi(
return df_final[df_final["open_trades"] > max_open_trades] return df_final[df_final["open_trades"] > max_open_trades]
def trade_list_to_dataframe(trades: Union[list[Trade], list[LocalTrade]]) -> pd.DataFrame: def trade_list_to_dataframe(trades: list[Trade] | list[LocalTrade]) -> pd.DataFrame:
""" """
Convert list of Trade objects to pandas Dataframe Convert list of Trade objects to pandas Dataframe
:param trades: List of trade objects :param trades: List of trade objects
@@ -453,7 +451,7 @@ def trade_list_to_dataframe(trades: Union[list[Trade], list[LocalTrade]]) -> pd.
return df return df
def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataFrame: def load_trades_from_db(db_url: str, strategy: str | None = None) -> pd.DataFrame:
""" """
Load trades from a DB (using dburl) Load trades from a DB (using dburl)
:param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite)
@@ -476,7 +474,7 @@ def load_trades(
db_url: str, db_url: str,
exportfilename: Path, exportfilename: Path,
no_trades: bool = False, no_trades: bool = False,
strategy: Optional[str] = None, strategy: str | None = None,
) -> pd.DataFrame: ) -> pd.DataFrame:
""" """
Based on configuration option 'trade_source': Based on configuration option 'trade_source':

View File

@@ -8,7 +8,7 @@ Common Interface for bot and strategy to access data.
import logging import logging
from collections import deque from collections import deque
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Optional from typing import Any
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
@@ -40,17 +40,17 @@ class DataProvider:
def __init__( def __init__(
self, self,
config: Config, config: Config,
exchange: Optional[Exchange], exchange: Exchange | None,
pairlists=None, pairlists=None,
rpc: Optional[RPCManager] = None, rpc: RPCManager | None = None,
) -> None: ) -> None:
self._config = config self._config = config
self._exchange = exchange self._exchange = exchange
self._pairlists = pairlists self._pairlists = pairlists
self.__rpc = rpc self.__rpc = rpc
self.__cached_pairs: dict[PairWithTimeframe, tuple[DataFrame, datetime]] = {} self.__cached_pairs: dict[PairWithTimeframe, tuple[DataFrame, datetime]] = {}
self.__slice_index: Optional[int] = None self.__slice_index: int | None = None
self.__slice_date: Optional[datetime] = None self.__slice_date: datetime | None = None
self.__cached_pairs_backtesting: dict[PairWithTimeframe, DataFrame] = {} self.__cached_pairs_backtesting: dict[PairWithTimeframe, DataFrame] = {}
self.__producer_pairs_df: dict[ self.__producer_pairs_df: dict[
@@ -255,8 +255,8 @@ class DataProvider:
def get_producer_df( def get_producer_df(
self, self,
pair: str, pair: str,
timeframe: Optional[str] = None, timeframe: str | None = None,
candle_type: Optional[CandleType] = None, candle_type: CandleType | None = None,
producer_name: str = "default", producer_name: str = "default",
) -> tuple[DataFrame, datetime]: ) -> tuple[DataFrame, datetime]:
""" """
@@ -349,7 +349,7 @@ class DataProvider:
return total_candles return total_candles
def get_pair_dataframe( def get_pair_dataframe(
self, pair: str, timeframe: Optional[str] = None, candle_type: str = "" self, pair: str, timeframe: str | None = None, candle_type: str = ""
) -> DataFrame: ) -> DataFrame:
""" """
Return pair candle (OHLCV) data, either live or cached historical -- depending Return pair candle (OHLCV) data, either live or cached historical -- depending
@@ -437,7 +437,7 @@ class DataProvider:
def refresh( def refresh(
self, self,
pairlist: ListPairsWithTimeframes, pairlist: ListPairsWithTimeframes,
helping_pairs: Optional[ListPairsWithTimeframes] = None, helping_pairs: ListPairsWithTimeframes | None = None,
) -> None: ) -> None:
""" """
Refresh data, called with each cycle Refresh data, called with each cycle
@@ -471,7 +471,7 @@ class DataProvider:
return list(self._exchange._klines.keys()) return list(self._exchange._klines.keys())
def ohlcv( def ohlcv(
self, pair: str, timeframe: Optional[str] = None, copy: bool = True, candle_type: str = "" self, pair: str, timeframe: str | None = None, copy: bool = True, candle_type: str = ""
) -> DataFrame: ) -> DataFrame:
""" """
Get candle (OHLCV) data for the given pair as DataFrame Get candle (OHLCV) data for the given pair as DataFrame
@@ -497,7 +497,7 @@ class DataProvider:
return DataFrame() return DataFrame()
def trades( def trades(
self, pair: str, timeframe: Optional[str] = None, copy: bool = True, candle_type: str = "" self, pair: str, timeframe: str | None = None, copy: bool = True, candle_type: str = ""
) -> DataFrame: ) -> DataFrame:
""" """
Get candle (TRADES) data for the given pair as DataFrame Get candle (TRADES) data for the given pair as DataFrame
@@ -529,7 +529,7 @@ class DataProvider:
) )
return trades_df return trades_df
def market(self, pair: str) -> Optional[dict[str, Any]]: def market(self, pair: str) -> dict[str, Any] | None:
""" """
Return market data for the pair Return market data for the pair
:param pair: Pair to get the data for :param pair: Pair to get the data for

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import Optional
from pandas import DataFrame, read_feather, to_datetime from pandas import DataFrame, read_feather, to_datetime
@@ -37,7 +36,7 @@ class FeatherDataHandler(IDataHandler):
) )
def _ohlcv_load( def _ohlcv_load(
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType self, pair: str, timeframe: str, timerange: TimeRange | None, candle_type: CandleType
) -> DataFrame: ) -> DataFrame:
""" """
Internal method used to load data for one pair from disk. Internal method used to load data for one pair from disk.
@@ -108,7 +107,7 @@ class FeatherDataHandler(IDataHandler):
raise NotImplementedError() raise NotImplementedError()
def _trades_load( def _trades_load(
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
) -> DataFrame: ) -> DataFrame:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import Optional
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@@ -45,7 +44,7 @@ class HDF5DataHandler(IDataHandler):
) )
def _ohlcv_load( def _ohlcv_load(
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType self, pair: str, timeframe: str, timerange: TimeRange | None, candle_type: CandleType
) -> pd.DataFrame: ) -> pd.DataFrame:
""" """
Internal method used to load data for one pair from disk. Internal method used to load data for one pair from disk.
@@ -134,7 +133,7 @@ class HDF5DataHandler(IDataHandler):
raise NotImplementedError() raise NotImplementedError()
def _trades_load( def _trades_load(
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
) -> pd.DataFrame: ) -> pd.DataFrame:
""" """
Load a pair from h5 file. Load a pair from h5 file.

View File

@@ -10,7 +10,6 @@ from abc import ABC, abstractmethod
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Optional
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
@@ -126,7 +125,7 @@ class IDataHandler(ABC):
@abstractmethod @abstractmethod
def _ohlcv_load( def _ohlcv_load(
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType self, pair: str, timeframe: str, timerange: TimeRange | None, candle_type: CandleType
) -> DataFrame: ) -> DataFrame:
""" """
Internal method used to load data for one pair from disk. Internal method used to load data for one pair from disk.
@@ -247,7 +246,7 @@ class IDataHandler(ABC):
@abstractmethod @abstractmethod
def _trades_load( def _trades_load(
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
) -> DataFrame: ) -> DataFrame:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json
@@ -282,7 +281,7 @@ class IDataHandler(ABC):
return False return False
def trades_load( def trades_load(
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
) -> DataFrame: ) -> DataFrame:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json
@@ -370,7 +369,7 @@ class IDataHandler(ABC):
timeframe: str, timeframe: str,
candle_type: CandleType, candle_type: CandleType,
*, *,
timerange: Optional[TimeRange] = None, timerange: TimeRange | None = None,
fill_missing: bool = True, fill_missing: bool = True,
drop_incomplete: bool = False, drop_incomplete: bool = False,
startup_candles: int = 0, startup_candles: int = 0,
@@ -566,7 +565,7 @@ def get_datahandlerclass(datatype: str) -> type[IDataHandler]:
def get_datahandler( def get_datahandler(
datadir: Path, data_format: Optional[str] = None, data_handler: Optional[IDataHandler] = None datadir: Path, data_format: str | None = None, data_handler: IDataHandler | None = None
) -> IDataHandler: ) -> IDataHandler:
""" """
:param datadir: Folder to save data :param datadir: Folder to save data

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import Optional
import numpy as np import numpy as np
from pandas import DataFrame, read_json, to_datetime from pandas import DataFrame, read_json, to_datetime
@@ -45,7 +44,7 @@ class JsonDataHandler(IDataHandler):
) )
def _ohlcv_load( def _ohlcv_load(
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType self, pair: str, timeframe: str, timerange: TimeRange | None, candle_type: CandleType
) -> DataFrame: ) -> DataFrame:
""" """
Internal method used to load data for one pair from disk. Internal method used to load data for one pair from disk.
@@ -119,7 +118,7 @@ class JsonDataHandler(IDataHandler):
raise NotImplementedError() raise NotImplementedError()
def _trades_load( def _trades_load(
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
) -> DataFrame: ) -> DataFrame:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import Optional
from pandas import DataFrame, read_parquet, to_datetime from pandas import DataFrame, read_parquet, to_datetime
@@ -35,7 +34,7 @@ class ParquetDataHandler(IDataHandler):
data.reset_index(drop=True).loc[:, self._columns].to_parquet(filename) data.reset_index(drop=True).loc[:, self._columns].to_parquet(filename)
def _ohlcv_load( def _ohlcv_load(
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType self, pair: str, timeframe: str, timerange: TimeRange | None, candle_type: CandleType
) -> DataFrame: ) -> DataFrame:
""" """
Internal method used to load data for one pair from disk. Internal method used to load data for one pair from disk.
@@ -106,7 +105,7 @@ class ParquetDataHandler(IDataHandler):
raise NotImplementedError() raise NotImplementedError()
def _trades_load( def _trades_load(
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
) -> DataFrame: ) -> DataFrame:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json

View File

@@ -2,7 +2,6 @@ import logging
import operator import operator
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Optional
from pandas import DataFrame, concat from pandas import DataFrame, concat
@@ -37,12 +36,12 @@ def load_pair_history(
timeframe: str, timeframe: str,
datadir: Path, datadir: Path,
*, *,
timerange: Optional[TimeRange] = None, timerange: TimeRange | None = None,
fill_up_missing: bool = True, fill_up_missing: bool = True,
drop_incomplete: bool = False, drop_incomplete: bool = False,
startup_candles: int = 0, startup_candles: int = 0,
data_format: Optional[str] = None, data_format: str | None = None,
data_handler: Optional[IDataHandler] = None, data_handler: IDataHandler | None = None,
candle_type: CandleType = CandleType.SPOT, candle_type: CandleType = CandleType.SPOT,
) -> DataFrame: ) -> DataFrame:
""" """
@@ -79,13 +78,13 @@ def load_data(
timeframe: str, timeframe: str,
pairs: list[str], pairs: list[str],
*, *,
timerange: Optional[TimeRange] = None, timerange: TimeRange | None = None,
fill_up_missing: bool = True, fill_up_missing: bool = True,
startup_candles: int = 0, startup_candles: int = 0,
fail_without_data: bool = False, fail_without_data: bool = False,
data_format: str = "feather", data_format: str = "feather",
candle_type: CandleType = CandleType.SPOT, candle_type: CandleType = CandleType.SPOT,
user_futures_funding_rate: Optional[int] = None, user_futures_funding_rate: int | None = None,
) -> dict[str, DataFrame]: ) -> dict[str, DataFrame]:
""" """
Load ohlcv history data for a list of pairs. Load ohlcv history data for a list of pairs.
@@ -137,8 +136,8 @@ def refresh_data(
timeframe: str, timeframe: str,
pairs: list[str], pairs: list[str],
exchange: Exchange, exchange: Exchange,
data_format: Optional[str] = None, data_format: str | None = None,
timerange: Optional[TimeRange] = None, timerange: TimeRange | None = None,
candle_type: CandleType, candle_type: CandleType,
) -> None: ) -> None:
""" """
@@ -168,11 +167,11 @@ def refresh_data(
def _load_cached_data_for_updating( def _load_cached_data_for_updating(
pair: str, pair: str,
timeframe: str, timeframe: str,
timerange: Optional[TimeRange], timerange: TimeRange | None,
data_handler: IDataHandler, data_handler: IDataHandler,
candle_type: CandleType, candle_type: CandleType,
prepend: bool = False, prepend: bool = False,
) -> tuple[DataFrame, Optional[int], Optional[int]]: ) -> tuple[DataFrame, int | None, int | None]:
""" """
Load cached data to download more data. Load cached data to download more data.
If timerange is passed in, checks whether data from an before the stored data will be If timerange is passed in, checks whether data from an before the stored data will be
@@ -220,8 +219,8 @@ def _download_pair_history(
exchange: Exchange, exchange: Exchange,
timeframe: str = "5m", timeframe: str = "5m",
new_pairs_days: int = 30, new_pairs_days: int = 30,
data_handler: Optional[IDataHandler] = None, data_handler: IDataHandler | None = None,
timerange: Optional[TimeRange] = None, timerange: TimeRange | None = None,
candle_type: CandleType, candle_type: CandleType,
erase: bool = False, erase: bool = False,
prepend: bool = False, prepend: bool = False,
@@ -322,10 +321,10 @@ def refresh_backtest_ohlcv_data(
timeframes: list[str], timeframes: list[str],
datadir: Path, datadir: Path,
trading_mode: str, trading_mode: str,
timerange: Optional[TimeRange] = None, timerange: TimeRange | None = None,
new_pairs_days: int = 30, new_pairs_days: int = 30,
erase: bool = False, erase: bool = False,
data_format: Optional[str] = None, data_format: str | None = None,
prepend: bool = False, prepend: bool = False,
) -> list[str]: ) -> list[str]:
""" """
@@ -404,7 +403,7 @@ def _download_trades_history(
pair: str, pair: str,
*, *,
new_pairs_days: int = 30, new_pairs_days: int = 30,
timerange: Optional[TimeRange] = None, timerange: TimeRange | None = None,
data_handler: IDataHandler, data_handler: IDataHandler,
trading_mode: TradingMode, trading_mode: TradingMode,
) -> bool: ) -> bool:

View File

@@ -3,7 +3,6 @@
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Optional
import ccxt import ccxt
@@ -53,7 +52,7 @@ class Binance(Exchange):
(TradingMode.FUTURES, MarginMode.ISOLATED) (TradingMode.FUTURES, MarginMode.ISOLATED)
] ]
def get_tickers(self, symbols: Optional[list[str]] = None, cached: bool = False) -> Tickers: def get_tickers(self, symbols: list[str] | None = None, cached: bool = False) -> Tickers:
tickers = super().get_tickers(symbols=symbols, cached=cached) tickers = super().get_tickers(symbols=symbols, cached=cached)
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
# Binance's future result has no bid/ask values. # Binance's future result has no bid/ask values.
@@ -106,7 +105,7 @@ class Binance(Exchange):
candle_type: CandleType, candle_type: CandleType,
is_new_pair: bool = False, is_new_pair: bool = False,
raise_: bool = False, raise_: bool = False,
until_ms: Optional[int] = None, until_ms: int | None = None,
) -> OHLCVResponse: ) -> OHLCVResponse:
""" """
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
@@ -144,9 +143,7 @@ class Binance(Exchange):
""" """
return open_date.minute == 0 and open_date.second < 15 return open_date.minute == 0 and open_date.second < 15
def fetch_funding_rates( def fetch_funding_rates(self, symbols: list[str] | None = None) -> dict[str, dict[str, float]]:
self, symbols: Optional[list[str]] = None
) -> dict[str, dict[str, float]]:
""" """
Fetch funding rates for the given symbols. Fetch funding rates for the given symbols.
:param symbols: List of symbols to fetch funding rates for :param symbols: List of symbols to fetch funding rates for
@@ -177,7 +174,7 @@ class Binance(Exchange):
leverage: float, leverage: float,
wallet_balance: float, # Or margin balance wallet_balance: float, # Or margin balance
open_trades: list, open_trades: list,
) -> Optional[float]: ) -> float | None:
""" """
Important: Must be fetching data from cached values as this is used by backtesting! Important: Must be fetching data from cached values as this is used by backtesting!
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@@ -17,7 +16,7 @@ class Bitpanda(Exchange):
""" """
def get_trades_for_order( def get_trades_for_order(
self, order_id: str, pair: str, since: datetime, params: Optional[dict] = None self, order_id: str, pair: str, since: datetime, params: dict | None = None
) -> list: ) -> list:
""" """
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id. Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.

View File

@@ -2,7 +2,7 @@
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Optional from typing import Any
import ccxt import ccxt
@@ -11,7 +11,7 @@ from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import FtHas from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.util.datetime_helpers import dt_now, dt_ts from freqtrade.util.datetime_helpers import dt_now, dt_ts
@@ -115,9 +115,9 @@ class Bybit(Exchange):
raise OperationalException(e) from e raise OperationalException(e) from e
def ohlcv_candle_limit( def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
) -> int: ) -> int:
if candle_type in (CandleType.FUNDING_RATE): if candle_type == CandleType.FUNDING_RATE:
return 200 return 200
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms) return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
@@ -157,7 +157,7 @@ class Bybit(Exchange):
leverage: float, leverage: float,
wallet_balance: float, # Or margin balance wallet_balance: float, # Or margin balance
open_trades: list, open_trades: list,
) -> Optional[float]: ) -> float | None:
""" """
Important: Must be fetching data from cached values as this is used by backtesting! Important: Must be fetching data from cached values as this is used by backtesting!
PERPETUAL: PERPETUAL:
@@ -229,7 +229,9 @@ class Bybit(Exchange):
logger.warning(f"Could not update funding fees for {pair}.") logger.warning(f"Could not update funding fees for {pair}.")
return 0.0 return 0.0
def fetch_orders(self, pair: str, since: datetime, params: Optional[dict] = None) -> list[dict]: def fetch_orders(
self, pair: str, since: datetime, params: dict | None = None
) -> list[CcxtOrder]:
""" """
Fetch all orders for a pair "since" Fetch all orders for a pair "since"
:param pair: Pair for the query :param pair: Pair for the query
@@ -246,7 +248,7 @@ class Bybit(Exchange):
return orders return orders
def fetch_order(self, order_id: str, pair: str, params: Optional[dict] = None) -> dict: def fetch_order(self, order_id: str, pair: str, params: dict | None = None) -> CcxtOrder:
if self.exchange_has("fetchOrder"): if self.exchange_has("fetchOrder"):
# Set acknowledged to True to avoid ccxt exception # Set acknowledged to True to avoid ccxt exception
params = {"acknowledged": True} params = {"acknowledged": True}

View File

@@ -1,8 +1,9 @@
import asyncio import asyncio
import logging import logging
import time import time
from collections.abc import Callable
from functools import wraps from functools import wraps
from typing import Any, Callable, Optional, TypeVar, cast, overload from typing import Any, TypeVar, cast, overload
from freqtrade.constants import ExchangeConfig from freqtrade.constants import ExchangeConfig
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
@@ -172,7 +173,7 @@ def retrier(_func: F, *, retries=API_RETRY_COUNT) -> F: ...
def retrier(*, retries=API_RETRY_COUNT) -> Callable[[F], F]: ... def retrier(*, retries=API_RETRY_COUNT) -> Callable[[F], F]: ...
def retrier(_func: Optional[F] = None, *, retries=API_RETRY_COUNT): def retrier(_func: F | None = None, *, retries=API_RETRY_COUNT):
def decorator(f: F) -> F: def decorator(f: F) -> F:
@wraps(f) @wraps(f)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
@@ -185,7 +186,7 @@ def retrier(_func: Optional[F] = None, *, retries=API_RETRY_COUNT):
logger.warning(msg + f"Retrying still for {count} times.") logger.warning(msg + f"Retrying still for {count} times.")
count -= 1 count -= 1
kwargs.update({"count": count}) kwargs.update({"count": count})
if isinstance(ex, (DDosProtection, RetryableOrderError)): if isinstance(ex, DDosProtection | RetryableOrderError):
# increasing backoff # increasing backoff
backoff_delay = calculate_backoff(count + 1, retries) backoff_delay = calculate_backoff(count + 1, retries)
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}") logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")

View File

@@ -12,7 +12,7 @@ from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import floor, isnan from math import floor, isnan
from threading import Lock from threading import Lock
from typing import Any, Literal, Optional, Union from typing import Any, Literal, TypeGuard
import ccxt import ccxt
import ccxt.pro as ccxt_pro import ccxt.pro as ccxt_pro
@@ -70,6 +70,7 @@ from freqtrade.exchange.common import (
) )
from freqtrade.exchange.exchange_types import ( from freqtrade.exchange.exchange_types import (
CcxtBalances, CcxtBalances,
CcxtOrder,
CcxtPosition, CcxtPosition,
FtHas, FtHas,
OHLCVResponse, OHLCVResponse,
@@ -169,7 +170,7 @@ class Exchange:
self, self,
config: Config, config: Config,
*, *,
exchange_config: Optional[ExchangeConfig] = None, exchange_config: ExchangeConfig | None = None,
validate: bool = True, validate: bool = True,
load_leverage_tiers: bool = False, load_leverage_tiers: bool = False,
) -> None: ) -> None:
@@ -181,7 +182,7 @@ class Exchange:
self._api: ccxt.Exchange self._api: ccxt.Exchange
self._api_async: ccxt_pro.Exchange self._api_async: ccxt_pro.Exchange
self._ws_async: ccxt_pro.Exchange = None self._ws_async: ccxt_pro.Exchange = None
self._exchange_ws: Optional[ExchangeWS] = None self._exchange_ws: ExchangeWS | None = None
self._markets: dict = {} self._markets: dict = {}
self._trading_fees: dict[str, Any] = {} self._trading_fees: dict[str, Any] = {}
self._leverage_tiers: dict[str, list[dict]] = {} self._leverage_tiers: dict[str, list[dict]] = {}
@@ -378,7 +379,7 @@ class Exchange:
logger.info("Applying additional ccxt config: %s", ccxt_kwargs) logger.info("Applying additional ccxt config: %s", ccxt_kwargs)
if self._ccxt_params: if self._ccxt_params:
# Inject static options after the above output to not confuse users. # Inject static options after the above output to not confuse users.
ccxt_kwargs = deep_merge_dicts(self._ccxt_params, ccxt_kwargs) ccxt_kwargs = deep_merge_dicts(self._ccxt_params, deepcopy(ccxt_kwargs))
if ccxt_kwargs: if ccxt_kwargs:
ex_config.update(ccxt_kwargs) ex_config.update(ccxt_kwargs)
try: try:
@@ -452,7 +453,7 @@ class Exchange:
logger.info(f"API {endpoint}: {add_info_str}{response}") logger.info(f"API {endpoint}: {add_info_str}{response}")
def ohlcv_candle_limit( def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
) -> int: ) -> int:
""" """
Exchange ohlcv candle limit Exchange ohlcv candle limit
@@ -472,8 +473,8 @@ class Exchange:
def get_markets( def get_markets(
self, self,
base_currencies: Optional[list[str]] = None, base_currencies: list[str] | None = None,
quote_currencies: Optional[list[str]] = None, quote_currencies: list[str] | None = None,
spot_only: bool = False, spot_only: bool = False,
margin_only: bool = False, margin_only: bool = False,
futures_only: bool = False, futures_only: bool = False,
@@ -566,7 +567,7 @@ class Exchange:
else: else:
return DataFrame(columns=DEFAULT_TRADES_COLUMNS) return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
def get_contract_size(self, pair: str) -> Optional[float]: def get_contract_size(self, pair: str) -> float | None:
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
market = self.markets.get(pair, {}) market = self.markets.get(pair, {})
contract_size: float = 1.0 contract_size: float = 1.0
@@ -587,7 +588,7 @@ class Exchange:
trade["amount"] = trade["amount"] * contract_size trade["amount"] = trade["amount"] * contract_size
return trades return trades
def _order_contracts_to_amount(self, order: dict) -> dict: def _order_contracts_to_amount(self, order: CcxtOrder) -> CcxtOrder:
if "symbol" in order and order["symbol"] is not None: if "symbol" in order and order["symbol"] is not None:
contract_size = self.get_contract_size(order["symbol"]) contract_size = self.get_contract_size(order["symbol"])
if contract_size != 1: if contract_size != 1:
@@ -709,7 +710,7 @@ class Exchange:
return pair return pair
raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
def validate_timeframes(self, timeframe: Optional[str]) -> None: def validate_timeframes(self, timeframe: str | None) -> None:
""" """
Check if timeframe from config is a supported timeframe on the exchange Check if timeframe from config is a supported timeframe on the exchange
""" """
@@ -839,7 +840,7 @@ class Exchange:
def validate_trading_mode_and_margin_mode( def validate_trading_mode_and_margin_mode(
self, self,
trading_mode: TradingMode, trading_mode: TradingMode,
margin_mode: Optional[MarginMode], # Only None when trading_mode = TradingMode.SPOT margin_mode: MarginMode | None, # Only None when trading_mode = TradingMode.SPOT
): ):
""" """
Checks if freqtrade can perform trades using the configured Checks if freqtrade can perform trades using the configured
@@ -855,7 +856,7 @@ class Exchange:
f"Freqtrade does not support {mm_value} {trading_mode} on {self.name}" f"Freqtrade does not support {mm_value} {trading_mode} on {self.name}"
) )
def get_option(self, param: str, default: Optional[Any] = None) -> Any: def get_option(self, param: str, default: Any | None = None) -> Any:
""" """
Get parameter value from _ft_has Get parameter value from _ft_has
""" """
@@ -872,7 +873,7 @@ class Exchange:
return self._ft_has["exchange_has_overrides"][endpoint] return self._ft_has["exchange_has_overrides"][endpoint]
return endpoint in self._api_async.has and self._api_async.has[endpoint] return endpoint in self._api_async.has and self._api_async.has[endpoint]
def get_precision_amount(self, pair: str) -> Optional[float]: def get_precision_amount(self, pair: str) -> float | None:
""" """
Returns the amount precision of the exchange. Returns the amount precision of the exchange.
:param pair: Pair to get precision for :param pair: Pair to get precision for
@@ -880,7 +881,7 @@ class Exchange:
""" """
return self.markets.get(pair, {}).get("precision", {}).get("amount", None) return self.markets.get(pair, {}).get("precision", {}).get("amount", None)
def get_precision_price(self, pair: str) -> Optional[float]: def get_precision_price(self, pair: str) -> float | None:
""" """
Returns the price precision of the exchange. Returns the price precision of the exchange.
:param pair: Pair to get precision for :param pair: Pair to get precision for
@@ -920,8 +921,8 @@ class Exchange:
return 1 / pow(10, precision) return 1 / pow(10, precision)
def get_min_pair_stake_amount( def get_min_pair_stake_amount(
self, pair: str, price: float, stoploss: float, leverage: Optional[float] = 1.0 self, pair: str, price: float, stoploss: float, leverage: float | None = 1.0
) -> Optional[float]: ) -> float | None:
return self._get_stake_amount_limit(pair, price, stoploss, "min", leverage) return self._get_stake_amount_limit(pair, price, stoploss, "min", leverage)
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float: def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
@@ -939,8 +940,8 @@ class Exchange:
price: float, price: float,
stoploss: float, stoploss: float,
limit: Literal["min", "max"], limit: Literal["min", "max"],
leverage: Optional[float] = 1.0, leverage: float | None = 1.0,
) -> Optional[float]: ) -> float | None:
isMin = limit == "min" isMin = limit == "min"
try: try:
@@ -997,20 +998,20 @@ class Exchange:
self, self,
pair: str, pair: str,
ordertype: str, ordertype: str,
side: str, side: BuySell,
amount: float, amount: float,
rate: float, rate: float,
leverage: float, leverage: float,
params: Optional[dict] = None, params: dict | None = None,
stop_loss: bool = False, stop_loss: bool = False,
) -> dict[str, Any]: ) -> CcxtOrder:
now = dt_now() now = dt_now()
order_id = f"dry_run_{side}_{pair}_{now.timestamp()}" order_id = f"dry_run_{side}_{pair}_{now.timestamp()}"
# Rounding here must respect to contract sizes # Rounding here must respect to contract sizes
_amount = self._contracts_to_amount( _amount = self._contracts_to_amount(
pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
) )
dry_order: dict[str, Any] = { dry_order: CcxtOrder = {
"id": order_id, "id": order_id,
"symbol": pair, "symbol": pair,
"price": rate, "price": rate,
@@ -1026,14 +1027,13 @@ class Exchange:
"status": "open", "status": "open",
"fee": None, "fee": None,
"info": {}, "info": {},
"leverage": leverage,
} }
if stop_loss: if stop_loss:
dry_order["info"] = {"stopPrice": dry_order["price"]} dry_order["info"] = {"stopPrice": dry_order["price"]}
dry_order[self._ft_has["stop_price_prop"]] = dry_order["price"] dry_order[self._ft_has["stop_price_prop"]] = dry_order["price"]
# Workaround to avoid filling stoploss orders immediately # Workaround to avoid filling stoploss orders immediately
dry_order["ft_order_type"] = "stoploss" dry_order["ft_order_type"] = "stoploss"
orderbook: Optional[OrderBook] = None orderbook: OrderBook | None = None
if self.exchange_has("fetchL2OrderBook"): if self.exchange_has("fetchL2OrderBook"):
orderbook = self.fetch_l2_order_book(pair, 20) orderbook = self.fetch_l2_order_book(pair, 20)
if ordertype == "limit" and orderbook: if ordertype == "limit" and orderbook:
@@ -1055,7 +1055,7 @@ class Exchange:
"filled": _amount, "filled": _amount,
"remaining": 0.0, "remaining": 0.0,
"status": "closed", "status": "closed",
"cost": (dry_order["amount"] * average), "cost": (_amount * average),
} }
) )
# market orders will always incurr taker fees # market orders will always incurr taker fees
@@ -1072,9 +1072,9 @@ class Exchange:
def add_dry_order_fee( def add_dry_order_fee(
self, self,
pair: str, pair: str,
dry_order: dict[str, Any], dry_order: CcxtOrder,
taker_or_maker: MakerTaker, taker_or_maker: MakerTaker,
) -> dict[str, Any]: ) -> CcxtOrder:
fee = self.get_fee(pair, taker_or_maker=taker_or_maker) fee = self.get_fee(pair, taker_or_maker=taker_or_maker)
dry_order.update( dry_order.update(
{ {
@@ -1088,7 +1088,7 @@ class Exchange:
return dry_order return dry_order
def get_dry_market_fill_price( def get_dry_market_fill_price(
self, pair: str, side: str, amount: float, rate: float, orderbook: Optional[OrderBook] self, pair: str, side: str, amount: float, rate: float, orderbook: OrderBook | None
) -> float: ) -> float:
""" """
Get the market order fill price based on orderbook interpolation Get the market order fill price based on orderbook interpolation
@@ -1136,7 +1136,7 @@ class Exchange:
pair: str, pair: str,
side: str, side: str,
limit: float, limit: float,
orderbook: Optional[OrderBook] = None, orderbook: OrderBook | None = None,
offset: float = 0.0, offset: float = 0.0,
) -> bool: ) -> bool:
if not self.exchange_has("fetchL2OrderBook"): if not self.exchange_has("fetchL2OrderBook"):
@@ -1158,8 +1158,8 @@ class Exchange:
return False return False
def check_dry_limit_order_filled( def check_dry_limit_order_filled(
self, order: dict[str, Any], immediate: bool = False, orderbook: Optional[OrderBook] = None self, order: CcxtOrder, immediate: bool = False, orderbook: OrderBook | None = None
) -> dict[str, Any]: ) -> CcxtOrder:
""" """
Check dry-run limit order fill and update fee (if it filled). Check dry-run limit order fill and update fee (if it filled).
""" """
@@ -1186,7 +1186,7 @@ class Exchange:
return order return order
def fetch_dry_run_order(self, order_id) -> dict[str, Any]: def fetch_dry_run_order(self, order_id) -> CcxtOrder:
""" """
Return dry-run order Return dry-run order
Only call if running in dry-run mode. Only call if running in dry-run mode.
@@ -1230,10 +1230,10 @@ class Exchange:
params.update({"reduceOnly": True}) params.update({"reduceOnly": True})
return params return params
def _order_needs_price(self, ordertype: str) -> bool: def _order_needs_price(self, side: BuySell, ordertype: str) -> bool:
return ( return (
ordertype != "market" ordertype != "market"
or self._api.options.get("createMarketBuyOrderRequiresPrice", False) or (side == "buy" and self._api.options.get("createMarketBuyOrderRequiresPrice", False))
or self._ft_has.get("marketOrderRequiresPrice", False) or self._ft_has.get("marketOrderRequiresPrice", False)
) )
@@ -1248,7 +1248,7 @@ class Exchange:
leverage: float, leverage: float,
reduceOnly: bool = False, reduceOnly: bool = False,
time_in_force: str = "GTC", time_in_force: str = "GTC",
) -> dict: ) -> CcxtOrder:
if self._config["dry_run"]: if self._config["dry_run"]:
dry_order = self.create_dry_run_order( dry_order = self.create_dry_run_order(
pair, ordertype, side, amount, self.price_to_precision(pair, rate), leverage pair, ordertype, side, amount, self.price_to_precision(pair, rate), leverage
@@ -1260,7 +1260,7 @@ class Exchange:
try: try:
# Set the precision for amount and price(rate) as accepted by the exchange # Set the precision for amount and price(rate) as accepted by the exchange
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
needs_price = self._order_needs_price(ordertype) needs_price = self._order_needs_price(side, ordertype)
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
if not reduceOnly: if not reduceOnly:
@@ -1306,7 +1306,7 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def stoploss_adjust(self, stop_loss: float, order: dict, side: str) -> bool: def stoploss_adjust(self, stop_loss: float, order: CcxtOrder, side: str) -> bool:
""" """
Verify stop_loss against stoploss-order value (limit or price) Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary. Returns True if adjustment is necessary.
@@ -1367,7 +1367,7 @@ class Exchange:
order_types: dict, order_types: dict,
side: BuySell, side: BuySell,
leverage: float, leverage: float,
) -> dict: ) -> CcxtOrder:
""" """
creates a stoploss order. creates a stoploss order.
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
@@ -1460,7 +1460,7 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def fetch_order_emulated(self, order_id: str, pair: str, params: dict) -> dict: def fetch_order_emulated(self, order_id: str, pair: str, params: dict) -> CcxtOrder:
""" """
Emulated fetch_order if the exchange doesn't support fetch_order, but requires separate Emulated fetch_order if the exchange doesn't support fetch_order, but requires separate
calls for open and closed orders. calls for open and closed orders.
@@ -1494,7 +1494,7 @@ class Exchange:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT) @retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
def fetch_order(self, order_id: str, pair: str, params: Optional[dict] = None) -> dict: def fetch_order(self, order_id: str, pair: str, params: dict | None = None) -> CcxtOrder:
if self._config["dry_run"]: if self._config["dry_run"]:
return self.fetch_dry_run_order(order_id) return self.fetch_dry_run_order(order_id)
if params is None: if params is None:
@@ -1523,12 +1523,14 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[dict] = None) -> dict: def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
return self.fetch_order(order_id, pair, params) return self.fetch_order(order_id, pair, params)
def fetch_order_or_stoploss_order( def fetch_order_or_stoploss_order(
self, order_id: str, pair: str, stoploss_order: bool = False self, order_id: str, pair: str, stoploss_order: bool = False
) -> dict: ) -> CcxtOrder:
""" """
Simple wrapper calling either fetch_order or fetch_stoploss_order depending on Simple wrapper calling either fetch_order or fetch_stoploss_order depending on
the stoploss_order parameter the stoploss_order parameter
@@ -1540,7 +1542,7 @@ class Exchange:
return self.fetch_stoploss_order(order_id, pair) return self.fetch_stoploss_order(order_id, pair)
return self.fetch_order(order_id, pair) return self.fetch_order(order_id, pair)
def check_order_canceled_empty(self, order: dict) -> bool: def check_order_canceled_empty(self, order: CcxtOrder) -> bool:
""" """
Verify if an order has been cancelled without being partially filled Verify if an order has been cancelled without being partially filled
:param order: Order dict as returned from fetch_order() :param order: Order dict as returned from fetch_order()
@@ -1549,7 +1551,7 @@ class Exchange:
return order.get("status") in NON_OPEN_EXCHANGE_STATES and order.get("filled") == 0.0 return order.get("status") in NON_OPEN_EXCHANGE_STATES and order.get("filled") == 0.0
@retrier @retrier
def cancel_order(self, order_id: str, pair: str, params: Optional[dict] = None) -> dict: def cancel_order(self, order_id: str, pair: str, params: dict | None = None) -> dict[str, Any]:
if self._config["dry_run"]: if self._config["dry_run"]:
try: try:
order = self.fetch_dry_run_order(order_id) order = self.fetch_dry_run_order(order_id)
@@ -1577,19 +1579,17 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def cancel_stoploss_order( def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
self, order_id: str, pair: str, params: Optional[dict] = None
) -> dict:
return self.cancel_order(order_id, pair, params) return self.cancel_order(order_id, pair, params)
def is_cancel_order_result_suitable(self, corder) -> bool: def is_cancel_order_result_suitable(self, corder) -> TypeGuard[CcxtOrder]:
if not isinstance(corder, dict): if not isinstance(corder, dict):
return False return False
required = ("fee", "status", "amount") required = ("fee", "status", "amount")
return all(corder.get(k, None) is not None for k in required) return all(corder.get(k, None) is not None for k in required)
def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> dict: def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> CcxtOrder:
""" """
Cancel order returning a result. Cancel order returning a result.
Creates a fake result if cancel order returns a non-usable result Creates a fake result if cancel order returns a non-usable result
@@ -1620,7 +1620,9 @@ class Exchange:
return order return order
def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> dict: def cancel_stoploss_order_with_result(
self, order_id: str, pair: str, amount: float
) -> CcxtOrder:
""" """
Cancel stoploss order returning a result. Cancel stoploss order returning a result.
Creates a fake result if cancel order returns a non-usable result Creates a fake result if cancel order returns a non-usable result
@@ -1662,7 +1664,7 @@ class Exchange:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier @retrier
def fetch_positions(self, pair: Optional[str] = None) -> list[CcxtPosition]: def fetch_positions(self, pair: str | None = None) -> list[CcxtPosition]:
""" """
Fetch positions from the exchange. Fetch positions from the exchange.
If no pair is given, all positions are returned. If no pair is given, all positions are returned.
@@ -1686,7 +1688,7 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[dict]: def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
orders = [] orders = []
if self.exchange_has("fetchClosedOrders"): if self.exchange_has("fetchClosedOrders"):
orders = self._api.fetch_closed_orders(pair, since=since_ms) orders = self._api.fetch_closed_orders(pair, since=since_ms)
@@ -1696,7 +1698,9 @@ class Exchange:
return orders return orders
@retrier(retries=0) @retrier(retries=0)
def fetch_orders(self, pair: str, since: datetime, params: Optional[dict] = None) -> list[dict]: def fetch_orders(
self, pair: str, since: datetime, params: dict | None = None
) -> list[CcxtOrder]:
""" """
Fetch all orders for a pair "since" Fetch all orders for a pair "since"
:param pair: Pair for the query :param pair: Pair for the query
@@ -1712,7 +1716,9 @@ class Exchange:
if not params: if not params:
params = {} params = {}
try: try:
orders: list[dict] = self._api.fetch_orders(pair, since=since_ms, params=params) orders: list[CcxtOrder] = self._api.fetch_orders(
pair, since=since_ms, params=params
)
except ccxt.NotSupported: except ccxt.NotSupported:
# Some exchanges don't support fetchOrders # Some exchanges don't support fetchOrders
# attempt to fetch open and closed orders separately # attempt to fetch open and closed orders separately
@@ -1757,7 +1763,7 @@ class Exchange:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier @retrier
def fetch_bids_asks(self, symbols: Optional[list[str]] = None, cached: bool = False) -> dict: def fetch_bids_asks(self, symbols: list[str] | None = None, cached: bool = False) -> dict:
""" """
:param symbols: List of symbols to fetch :param symbols: List of symbols to fetch
:param cached: Allow cached result :param cached: Allow cached result
@@ -1790,7 +1796,7 @@ class Exchange:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier @retrier
def get_tickers(self, symbols: Optional[list[str]] = None, cached: bool = False) -> Tickers: def get_tickers(self, symbols: list[str] | None = None, cached: bool = False) -> Tickers:
""" """
:param cached: Allow cached result :param cached: Allow cached result
:return: fetch_tickers result :return: fetch_tickers result
@@ -1850,7 +1856,7 @@ class Exchange:
@staticmethod @staticmethod
def get_next_limit_in_list( def get_next_limit_in_list(
limit: int, limit_range: Optional[list[int]], range_required: bool = True limit: int, limit_range: list[int] | None, range_required: bool = True
): ):
""" """
Get next greater value in the list. Get next greater value in the list.
@@ -1914,8 +1920,8 @@ class Exchange:
refresh: bool, refresh: bool,
side: EntryExit, side: EntryExit,
is_short: bool, is_short: bool,
order_book: Optional[OrderBook] = None, order_book: OrderBook | None = None,
ticker: Optional[Ticker] = None, ticker: Ticker | None = None,
) -> float: ) -> float:
""" """
Calculates bid/ask target Calculates bid/ask target
@@ -1964,7 +1970,7 @@ class Exchange:
def _get_rate_from_ticker( def _get_rate_from_ticker(
self, side: EntryExit, ticker: Ticker, conf_strategy: dict[str, Any], price_side: BidAsk self, side: EntryExit, ticker: Ticker, conf_strategy: dict[str, Any], price_side: BidAsk
) -> Optional[float]: ) -> float | None:
""" """
Get rate from ticker. Get rate from ticker.
""" """
@@ -2043,7 +2049,7 @@ class Exchange:
@retrier @retrier
def get_trades_for_order( def get_trades_for_order(
self, order_id: str, pair: str, since: datetime, params: Optional[dict] = None self, order_id: str, pair: str, since: datetime, params: dict | None = None
) -> list: ) -> list:
""" """
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id. Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
@@ -2090,7 +2096,7 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def get_order_id_conditional(self, order: dict[str, Any]) -> str: def get_order_id_conditional(self, order: CcxtOrder) -> str:
return order["id"] return order["id"]
@retrier @retrier
@@ -2139,7 +2145,7 @@ class Exchange:
raise OperationalException(e) from e raise OperationalException(e) from e
@staticmethod @staticmethod
def order_has_fee(order: dict) -> bool: def order_has_fee(order: CcxtOrder) -> bool:
""" """
Verifies if the passed in order dict has the needed keys to extract fees, Verifies if the passed in order dict has the needed keys to extract fees,
and that these keys (currency, cost) are not empty. and that these keys (currency, cost) are not empty.
@@ -2158,7 +2164,7 @@ class Exchange:
def calculate_fee_rate( def calculate_fee_rate(
self, fee: dict, symbol: str, cost: float, amount: float self, fee: dict, symbol: str, cost: float, amount: float
) -> Optional[float]: ) -> float | None:
""" """
Calculate fee rate if it's not given by the exchange. Calculate fee rate if it's not given by the exchange.
:param fee: ccxt Fee dict - must contain cost / currency / rate :param fee: ccxt Fee dict - must contain cost / currency / rate
@@ -2197,8 +2203,8 @@ class Exchange:
return round((fee_cost * fee_to_quote_rate) / cost, 8) return round((fee_cost * fee_to_quote_rate) / cost, 8)
def extract_cost_curr_rate( def extract_cost_curr_rate(
self, fee: dict, symbol: str, cost: float, amount: float self, fee: dict[str, Any], symbol: str, cost: float, amount: float
) -> tuple[float, str, Optional[float]]: ) -> tuple[float, str, float | None]:
""" """
Extract tuple of cost, currency, rate. Extract tuple of cost, currency, rate.
Requires order_has_fee to run first! Requires order_has_fee to run first!
@@ -2223,7 +2229,7 @@ class Exchange:
since_ms: int, since_ms: int,
candle_type: CandleType, candle_type: CandleType,
is_new_pair: bool = False, is_new_pair: bool = False,
until_ms: Optional[int] = None, until_ms: int | None = None,
) -> DataFrame: ) -> DataFrame:
""" """
Get candle history using asyncio and returns the list of candles. Get candle history using asyncio and returns the list of candles.
@@ -2257,7 +2263,7 @@ class Exchange:
candle_type: CandleType, candle_type: CandleType,
is_new_pair: bool = False, is_new_pair: bool = False,
raise_: bool = False, raise_: bool = False,
until_ms: Optional[int] = None, until_ms: int | None = None,
) -> OHLCVResponse: ) -> OHLCVResponse:
""" """
Download historic ohlcv Download historic ohlcv
@@ -2302,7 +2308,7 @@ class Exchange:
pair: str, pair: str,
timeframe: str, timeframe: str,
candle_type: CandleType, candle_type: CandleType,
since_ms: Optional[int], since_ms: int | None,
cache: bool, cache: bool,
) -> Coroutine[Any, Any, OHLCVResponse]: ) -> Coroutine[Any, Any, OHLCVResponse]:
not_all_data = cache and self.required_candle_call_count > 1 not_all_data = cache and self.required_candle_call_count > 1
@@ -2371,7 +2377,7 @@ class Exchange:
) )
def _build_ohlcv_dl_jobs( def _build_ohlcv_dl_jobs(
self, pair_list: ListPairsWithTimeframes, since_ms: Optional[int], cache: bool self, pair_list: ListPairsWithTimeframes, since_ms: int | None, cache: bool
) -> tuple[list[Coroutine], list[PairWithTimeframe]]: ) -> tuple[list[Coroutine], list[PairWithTimeframe]]:
""" """
Build Coroutines to execute as part of refresh_latest_ohlcv Build Coroutines to execute as part of refresh_latest_ohlcv
@@ -2448,9 +2454,9 @@ class Exchange:
self, self,
pair_list: ListPairsWithTimeframes, pair_list: ListPairsWithTimeframes,
*, *,
since_ms: Optional[int] = None, since_ms: int | None = None,
cache: bool = True, cache: bool = True,
drop_incomplete: Optional[bool] = None, drop_incomplete: bool | None = None,
) -> dict[PairWithTimeframe, DataFrame]: ) -> dict[PairWithTimeframe, DataFrame]:
""" """
Refresh in-memory OHLCV asynchronously and set `_klines` with the result Refresh in-memory OHLCV asynchronously and set `_klines` with the result
@@ -2544,7 +2550,7 @@ class Exchange:
pair: str, pair: str,
timeframe: str, timeframe: str,
candle_type: CandleType, candle_type: CandleType,
since_ms: Optional[int] = None, since_ms: int | None = None,
) -> OHLCVResponse: ) -> OHLCVResponse:
""" """
Asynchronously get candle history data using fetch_ohlcv Asynchronously get candle history data using fetch_ohlcv
@@ -2618,7 +2624,7 @@ class Exchange:
pair: str, pair: str,
timeframe: str, timeframe: str,
limit: int, limit: int,
since_ms: Optional[int] = None, since_ms: int | None = None,
) -> list[list]: ) -> list[list]:
""" """
Fetch funding rate history - used to selectively override this by subclasses. Fetch funding rate history - used to selectively override this by subclasses.
@@ -2677,7 +2683,7 @@ class Exchange:
async def _build_trades_dl_jobs( async def _build_trades_dl_jobs(
self, pairwt: PairWithTimeframe, data_handler, cache: bool self, pairwt: PairWithTimeframe, data_handler, cache: bool
) -> tuple[PairWithTimeframe, Optional[DataFrame]]: ) -> tuple[PairWithTimeframe, DataFrame | None]:
""" """
Build coroutines to refresh trades for (they're then called through async.gather) Build coroutines to refresh trades for (they're then called through async.gather)
""" """
@@ -2821,7 +2827,7 @@ class Exchange:
@retrier_async @retrier_async
async def _async_fetch_trades( async def _async_fetch_trades(
self, pair: str, since: Optional[int] = None, params: Optional[dict] = None self, pair: str, since: int | None = None, params: dict | None = None
) -> tuple[list[list], Any]: ) -> tuple[list[list], Any]:
""" """
Asynchronously gets trade history using fetch_trades. Asynchronously gets trade history using fetch_trades.
@@ -2881,7 +2887,7 @@ class Exchange:
return trades[-1].get("timestamp") return trades[-1].get("timestamp")
async def _async_get_trade_history_id( async def _async_get_trade_history_id(
self, pair: str, until: int, since: Optional[int] = None, from_id: Optional[str] = None self, pair: str, until: int, since: int | None = None, from_id: str | None = None
) -> tuple[str, list[list]]: ) -> tuple[str, list[list]]:
""" """
Asynchronously gets trade history using fetch_trades Asynchronously gets trade history using fetch_trades
@@ -2936,7 +2942,7 @@ class Exchange:
return (pair, trades) return (pair, trades)
async def _async_get_trade_history_time( async def _async_get_trade_history_time(
self, pair: str, until: int, since: Optional[int] = None self, pair: str, until: int, since: int | None = None
) -> tuple[str, list[list]]: ) -> tuple[str, list[list]]:
""" """
Asynchronously gets trade history using fetch_trades, Asynchronously gets trade history using fetch_trades,
@@ -2977,9 +2983,9 @@ class Exchange:
async def _async_get_trade_history( async def _async_get_trade_history(
self, self,
pair: str, pair: str,
since: Optional[int] = None, since: int | None = None,
until: Optional[int] = None, until: int | None = None,
from_id: Optional[str] = None, from_id: str | None = None,
) -> tuple[str, list[list]]: ) -> tuple[str, list[list]]:
""" """
Async wrapper handling downloading trades using either time or id based methods. Async wrapper handling downloading trades using either time or id based methods.
@@ -3008,9 +3014,9 @@ class Exchange:
def get_historic_trades( def get_historic_trades(
self, self,
pair: str, pair: str,
since: Optional[int] = None, since: int | None = None,
until: Optional[int] = None, until: int | None = None,
from_id: Optional[str] = None, from_id: str | None = None,
) -> tuple[str, list]: ) -> tuple[str, list]:
""" """
Get trade history data using asyncio. Get trade history data using asyncio.
@@ -3039,7 +3045,7 @@ class Exchange:
return self.loop.run_until_complete(task) return self.loop.run_until_complete(task)
@retrier @retrier
def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float: def _get_funding_fees_from_exchange(self, pair: str, since: datetime | int) -> float:
""" """
Returns the sum of all funding fees that were exchanged for a pair within a timeframe Returns the sum of all funding fees that were exchanged for a pair within a timeframe
Dry-run handling happens as part of _calculate_funding_fees. Dry-run handling happens as part of _calculate_funding_fees.
@@ -3170,8 +3176,8 @@ class Exchange:
file_dump_json(filename, data) file_dump_json(filename, data)
def load_cached_leverage_tiers( def load_cached_leverage_tiers(
self, stake_currency: str, cache_time: Optional[timedelta] = None self, stake_currency: str, cache_time: timedelta | None = None
) -> Optional[dict[str, list[dict]]]: ) -> dict[str, list[dict]] | None:
""" """
Load cached leverage tiers from disk Load cached leverage tiers from disk
:param cache_time: The maximum age of the cache before it is considered outdated :param cache_time: The maximum age of the cache before it is considered outdated
@@ -3216,7 +3222,7 @@ class Exchange:
"maintAmt": float(info["cum"]) if "cum" in info else None, "maintAmt": float(info["cum"]) if "cum" in info else None,
} }
def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float: def get_max_leverage(self, pair: str, stake_amount: float | None) -> float:
""" """
Returns the maximum leverage that a pair can be traded at Returns the maximum leverage that a pair can be traded at
:param pair: The base/quote currency pair being traded :param pair: The base/quote currency pair being traded
@@ -3294,7 +3300,7 @@ class Exchange:
def _set_leverage( def _set_leverage(
self, self,
leverage: float, leverage: float,
pair: Optional[str] = None, pair: str | None = None,
accept_fail: bool = False, accept_fail: bool = False,
): ):
""" """
@@ -3346,7 +3352,7 @@ class Exchange:
pair: str, pair: str,
margin_mode: MarginMode, margin_mode: MarginMode,
accept_fail: bool = False, accept_fail: bool = False,
params: Optional[dict] = None, params: dict | None = None,
): ):
""" """
Set's the margin mode on the exchange to cross or isolated for a specific pair Set's the margin mode on the exchange to cross or isolated for a specific pair
@@ -3381,7 +3387,7 @@ class Exchange:
amount: float, amount: float,
is_short: bool, is_short: bool,
open_date: datetime, open_date: datetime,
close_date: Optional[datetime] = None, close_date: datetime | None = None,
) -> float: ) -> float:
""" """
Fetches and calculates the sum of all funding fees that occurred for a pair Fetches and calculates the sum of all funding fees that occurred for a pair
@@ -3434,7 +3440,7 @@ class Exchange:
@staticmethod @staticmethod
def combine_funding_and_mark( def combine_funding_and_mark(
funding_rates: DataFrame, mark_rates: DataFrame, futures_funding_rate: Optional[int] = None funding_rates: DataFrame, mark_rates: DataFrame, futures_funding_rate: int | None = None
) -> DataFrame: ) -> DataFrame:
""" """
Combine funding-rates and mark-rates dataframes Combine funding-rates and mark-rates dataframes
@@ -3475,7 +3481,7 @@ class Exchange:
is_short: bool, is_short: bool,
open_date: datetime, open_date: datetime,
close_date: datetime, close_date: datetime,
time_in_ratio: Optional[float] = None, time_in_ratio: float | None = None,
) -> float: ) -> float:
""" """
calculates the sum of all funding fees that occurred for a pair during a futures trade calculates the sum of all funding fees that occurred for a pair during a futures trade
@@ -3533,8 +3539,8 @@ class Exchange:
stake_amount: float, stake_amount: float,
leverage: float, leverage: float,
wallet_balance: float, wallet_balance: float,
open_trades: Optional[list] = None, open_trades: list | None = None,
) -> Optional[float]: ) -> float | None:
""" """
Set's the margin mode on the exchange to cross or isolated for a specific pair Set's the margin mode on the exchange to cross or isolated for a specific pair
""" """
@@ -3582,7 +3588,7 @@ class Exchange:
leverage: float, leverage: float,
wallet_balance: float, # Or margin balance wallet_balance: float, # Or margin balance
open_trades: list, open_trades: list,
) -> Optional[float]: ) -> float | None:
""" """
Important: Must be fetching data from cached values as this is used by backtesting! Important: Must be fetching data from cached values as this is used by backtesting!
PERPETUAL: PERPETUAL:
@@ -3633,7 +3639,7 @@ class Exchange:
self, self,
pair: str, pair: str,
notional_value: float, notional_value: float,
) -> tuple[float, Optional[float]]: ) -> tuple[float, float | None]:
""" """
Important: Must be fetching data from cached values as this is used by backtesting! Important: Must be fetching data from cached values as this is used by backtesting!
:param pair: Market symbol :param pair: Market symbol

View File

@@ -1,4 +1,4 @@
from typing import Optional, TypedDict from typing import Any, Literal, TypedDict
from freqtrade.enums import CandleType from freqtrade.enums import CandleType
@@ -11,7 +11,7 @@ class FtHas(TypedDict, total=False):
# Stoploss on exchange # Stoploss on exchange
stoploss_on_exchange: bool stoploss_on_exchange: bool
stop_price_param: str stop_price_param: str
stop_price_prop: str stop_price_prop: Literal["stopPrice", "stopLossPrice"]
stop_price_type_field: str stop_price_type_field: str
stop_price_type_value_mapping: dict stop_price_type_value_mapping: dict
stoploss_order_types: dict[str, str] stoploss_order_types: dict[str, str]
@@ -35,7 +35,7 @@ class FtHas(TypedDict, total=False):
trades_has_history: bool trades_has_history: bool
trades_pagination_overlap: bool trades_pagination_overlap: bool
# Orderbook # Orderbook
l2_limit_range: Optional[list[int]] l2_limit_range: list[int] | None
l2_limit_range_required: bool l2_limit_range_required: bool
# Futures # Futures
ccxt_futures_name: str # usually swap ccxt_futures_name: str # usually swap
@@ -44,7 +44,7 @@ class FtHas(TypedDict, total=False):
funding_fee_timeframe: str funding_fee_timeframe: str
floor_leverage: bool floor_leverage: bool
needs_trading_fees: bool needs_trading_fees: bool
order_props_in_contracts: list[str] order_props_in_contracts: list[Literal["amount", "cost", "filled", "remaining"]]
# Websocket control # Websocket control
ws_enabled: bool ws_enabled: bool
@@ -52,14 +52,14 @@ class FtHas(TypedDict, total=False):
class Ticker(TypedDict): class Ticker(TypedDict):
symbol: str symbol: str
ask: Optional[float] ask: float | None
askVolume: Optional[float] askVolume: float | None
bid: Optional[float] bid: float | None
bidVolume: Optional[float] bidVolume: float | None
last: Optional[float] last: float | None
quoteVolume: Optional[float] quoteVolume: float | None
baseVolume: Optional[float] baseVolume: float | None
percentage: Optional[float] percentage: float | None
# Several more - only listing required. # Several more - only listing required.
@@ -70,9 +70,9 @@ class OrderBook(TypedDict):
symbol: str symbol: str
bids: list[tuple[float, float]] bids: list[tuple[float, float]]
asks: list[tuple[float, float]] asks: list[tuple[float, float]]
timestamp: Optional[int] timestamp: int | None
datetime: Optional[str] datetime: str | None
nonce: Optional[int] nonce: int | None
class CcxtBalance(TypedDict): class CcxtBalance(TypedDict):
@@ -89,10 +89,12 @@ class CcxtPosition(TypedDict):
side: str side: str
contracts: float contracts: float
leverage: float leverage: float
collateral: Optional[float] collateral: float | None
initialMargin: Optional[float] initialMargin: float | None
liquidationPrice: Optional[float] liquidationPrice: float | None
CcxtOrder = dict[str, Any]
# pair, timeframe, candleType, OHLCV, drop last?, # pair, timeframe, candleType, OHLCV, drop last?,
OHLCVResponse = tuple[str, str, CandleType, list, bool] OHLCVResponse = tuple[str, str, CandleType, list, bool]

View File

@@ -5,7 +5,7 @@ Exchange support utils
import inspect import inspect
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import ceil, floor from math import ceil, floor
from typing import Any, Optional from typing import Any
import ccxt import ccxt
from ccxt import ( from ccxt import (
@@ -33,20 +33,18 @@ from freqtrade.util import FtPrecise
CcxtModuleType = Any CcxtModuleType = Any
def is_exchange_known_ccxt( def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType | None = None) -> bool:
exchange_name: str, ccxt_module: Optional[CcxtModuleType] = None
) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module) return exchange_name in ccxt_exchanges(ccxt_module)
def ccxt_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> list[str]: def ccxt_exchanges(ccxt_module: CcxtModuleType | None = None) -> list[str]:
""" """
Return the list of all exchanges known to ccxt Return the list of all exchanges known to ccxt
""" """
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> list[str]: def available_exchanges(ccxt_module: CcxtModuleType | None = None) -> list[str]:
""" """
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
""" """
@@ -54,7 +52,7 @@ def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> list[st
return [x for x in exchanges if validate_exchange(x)[0]] return [x for x in exchanges if validate_exchange(x)[0]]
def validate_exchange(exchange: str) -> tuple[bool, str, Optional[ccxt.Exchange]]: def validate_exchange(exchange: str) -> tuple[bool, str, ccxt.Exchange | None]:
""" """
returns: can_use, reason, exchange_object returns: can_use, reason, exchange_object
with Reason including both missing and missing_opt with Reason including both missing and missing_opt
@@ -137,9 +135,7 @@ def list_available_exchanges(all_exchanges: bool) -> list[ValidExchangesType]:
return exchanges_valid return exchanges_valid
def date_minus_candles( def date_minus_candles(timeframe: str, candle_count: int, date: datetime | None = None) -> datetime:
timeframe: str, candle_count: int, date: Optional[datetime] = None
) -> datetime:
""" """
subtract X candles from a date. subtract X candles from a date.
:param timeframe: timeframe in string format (e.g. "5m") :param timeframe: timeframe in string format (e.g. "5m")
@@ -166,7 +162,7 @@ def market_is_active(market: dict) -> bool:
return market.get("active", True) is not False return market.get("active", True) is not False
def amount_to_contracts(amount: float, contract_size: Optional[float]) -> float: def amount_to_contracts(amount: float, contract_size: float | None) -> float:
""" """
Convert amount to contracts. Convert amount to contracts.
:param amount: amount to convert :param amount: amount to convert
@@ -179,7 +175,7 @@ def amount_to_contracts(amount: float, contract_size: Optional[float]) -> float:
return amount return amount
def contracts_to_amount(num_contracts: float, contract_size: Optional[float]) -> float: def contracts_to_amount(num_contracts: float, contract_size: float | None) -> float:
""" """
Takes num-contracts and converts it to contract size Takes num-contracts and converts it to contract size
:param num_contracts: number of contracts :param num_contracts: number of contracts
@@ -194,7 +190,7 @@ def contracts_to_amount(num_contracts: float, contract_size: Optional[float]) ->
def amount_to_precision( def amount_to_precision(
amount: float, amount_precision: Optional[float], precisionMode: Optional[int] amount: float, amount_precision: float | None, precisionMode: int | None
) -> float: ) -> float:
""" """
Returns the amount to buy or sell to a precision the Exchange accepts Returns the amount to buy or sell to a precision the Exchange accepts
@@ -224,9 +220,9 @@ def amount_to_precision(
def amount_to_contract_precision( def amount_to_contract_precision(
amount, amount,
amount_precision: Optional[float], amount_precision: float | None,
precisionMode: Optional[int], precisionMode: int | None,
contract_size: Optional[float], contract_size: float | None,
) -> float: ) -> float:
""" """
Returns the amount to buy or sell to a precision the Exchange accepts Returns the amount to buy or sell to a precision the Exchange accepts
@@ -285,8 +281,8 @@ def __price_to_precision_significant_digits(
def price_to_precision( def price_to_precision(
price: float, price: float,
price_precision: Optional[float], price_precision: float | None,
precisionMode: Optional[int], precisionMode: int | None,
*, *,
rounding_mode: int = ROUND, rounding_mode: int = ROUND,
) -> float: ) -> float:

View File

@@ -1,5 +1,4 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional
import ccxt import ccxt
from ccxt import ROUND_DOWN, ROUND_UP from ccxt import ROUND_DOWN, ROUND_UP
@@ -51,7 +50,7 @@ def timeframe_to_resample_freq(timeframe: str) -> str:
return resample_interval return resample_interval
def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> datetime: def timeframe_to_prev_date(timeframe: str, date: datetime | None = None) -> datetime:
""" """
Use Timeframe and determine the candle start date for this date. Use Timeframe and determine the candle start date for this date.
Does not round when given a candle start date. Does not round when given a candle start date.
@@ -66,7 +65,7 @@ def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> d
return dt_from_ts(new_timestamp) return dt_from_ts(new_timestamp)
def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> datetime: def timeframe_to_next_date(timeframe: str, date: datetime | None = None) -> datetime:
""" """
Use Timeframe and determine next candle. Use Timeframe and determine next candle.
:param timeframe: timeframe in string format (e.g. "5m") :param timeframe: timeframe in string format (e.g. "5m")

View File

@@ -2,12 +2,11 @@
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Any, Optional
from freqtrade.constants import BuySell from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, PriceType, TradingMode from freqtrade.enums import MarginMode, PriceType, TradingMode
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.misc import safe_value_fallback2 from freqtrade.misc import safe_value_fallback2
@@ -74,7 +73,7 @@ class Gate(Exchange):
return params return params
def get_trades_for_order( def get_trades_for_order(
self, order_id: str, pair: str, since: datetime, params: Optional[dict] = None self, order_id: str, pair: str, since: datetime, params: dict | None = None
) -> list: ) -> list:
trades = super().get_trades_for_order(order_id, pair, since, params) trades = super().get_trades_for_order(order_id, pair, since, params)
@@ -99,10 +98,12 @@ class Gate(Exchange):
} }
return trades return trades
def get_order_id_conditional(self, order: dict[str, Any]) -> str: def get_order_id_conditional(self, order: CcxtOrder) -> str:
return safe_value_fallback2(order, order, "id_stop", "id") return safe_value_fallback2(order, order, "id_stop", "id")
def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[dict] = None) -> dict: def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
order = self.fetch_order(order_id=order_id, pair=pair, params={"stop": True}) order = self.fetch_order(order_id=order_id, pair=pair, params={"stop": True})
if order.get("status", "open") == "closed": if order.get("status", "open") == "closed":
# Places a real order - which we need to fetch explicitly. # Places a real order - which we need to fetch explicitly.
@@ -119,7 +120,5 @@ class Gate(Exchange):
return order1 return order1
return order return order
def cancel_stoploss_order( def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
self, order_id: str, pair: str, params: Optional[dict] = None
) -> dict:
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True}) return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})

View File

@@ -2,7 +2,7 @@
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Any, Optional from typing import Any
import ccxt import ccxt
from pandas import DataFrame from pandas import DataFrame
@@ -50,7 +50,7 @@ class Kraken(Exchange):
return parent_check and market.get("darkpool", False) is False return parent_check and market.get("darkpool", False) is False
def get_tickers(self, symbols: Optional[list[str]] = None, cached: bool = False) -> Tickers: def get_tickers(self, symbols: list[str] | None = None, cached: bool = False) -> Tickers:
# Only fetch tickers for current stake currency # Only fetch tickers for current stake currency
# Otherwise the request for kraken becomes too large. # Otherwise the request for kraken becomes too large.
symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]])) symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]]))
@@ -99,7 +99,7 @@ class Kraken(Exchange):
def _set_leverage( def _set_leverage(
self, self,
leverage: float, leverage: float,
pair: Optional[str] = None, pair: str | None = None,
accept_fail: bool = False, accept_fail: bool = False,
): ):
""" """
@@ -137,7 +137,7 @@ class Kraken(Exchange):
is_short: bool, is_short: bool,
open_date: datetime, open_date: datetime,
close_date: datetime, close_date: datetime,
time_in_ratio: Optional[float] = None, time_in_ratio: float | None = None,
) -> float: ) -> float:
""" """
# ! This method will always error when run by Freqtrade because time_in_ratio is never # ! This method will always error when run by Freqtrade because time_in_ratio is never

View File

@@ -4,7 +4,7 @@ import logging
from freqtrade.constants import BuySell from freqtrade.constants import BuySell
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -47,7 +47,7 @@ class Kucoin(Exchange):
leverage: float, leverage: float,
reduceOnly: bool = False, reduceOnly: bool = False,
time_in_force: str = "GTC", time_in_force: str = "GTC",
) -> dict: ) -> CcxtOrder:
res = super().create_order( res = super().create_order(
pair=pair, pair=pair,
ordertype=ordertype, ordertype=ordertype,

View File

@@ -1,6 +1,5 @@
import logging import logging
from datetime import timedelta from datetime import timedelta
from typing import Any, Optional
import ccxt import ccxt
@@ -14,7 +13,7 @@ from freqtrade.exceptions import (
) )
from freqtrade.exchange import Exchange, date_minus_candles from freqtrade.exchange import Exchange, date_minus_candles
from freqtrade.exchange.common import API_RETRY_COUNT, retrier from freqtrade.exchange.common import API_RETRY_COUNT, retrier
from freqtrade.exchange.exchange_types import FtHas from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.misc import safe_value_fallback2 from freqtrade.misc import safe_value_fallback2
from freqtrade.util import dt_now, dt_ts from freqtrade.util import dt_now, dt_ts
@@ -60,7 +59,7 @@ class Okx(Exchange):
_ccxt_params: dict = {"options": {"brokerId": "ffb5405ad327SUDE"}} _ccxt_params: dict = {"options": {"brokerId": "ffb5405ad327SUDE"}}
def ohlcv_candle_limit( def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
) -> int: ) -> int:
""" """
Exchange ohlcv candle limit Exchange ohlcv candle limit
@@ -191,7 +190,7 @@ class Okx(Exchange):
params["posSide"] = self._get_posSide(side, True) params["posSide"] = self._get_posSide(side, True)
return params return params
def _convert_stop_order(self, pair: str, order_id: str, order: dict) -> dict: def _convert_stop_order(self, pair: str, order_id: str, order: CcxtOrder) -> CcxtOrder:
if ( if (
order.get("status", "open") == "closed" order.get("status", "open") == "closed"
and (real_order_id := order.get("info", {}).get("ordId")) is not None and (real_order_id := order.get("info", {}).get("ordId")) is not None
@@ -209,7 +208,9 @@ class Okx(Exchange):
return order return order
@retrier(retries=API_RETRY_COUNT) @retrier(retries=API_RETRY_COUNT)
def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[dict] = None) -> dict: def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
if self._config["dry_run"]: if self._config["dry_run"]:
return self.fetch_dry_run_order(order_id) return self.fetch_dry_run_order(order_id)
@@ -231,7 +232,7 @@ class Okx(Exchange):
return self._fetch_stop_order_fallback(order_id, pair) return self._fetch_stop_order_fallback(order_id, pair)
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> dict: def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> CcxtOrder:
params2 = {"stop": True, "ordType": "conditional"} params2 = {"stop": True, "ordType": "conditional"}
for method in ( for method in (
self._api.fetch_open_orders, self._api.fetch_open_orders,
@@ -256,14 +257,12 @@ class Okx(Exchange):
raise OperationalException(e) from e raise OperationalException(e) from e
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).") raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
def get_order_id_conditional(self, order: dict[str, Any]) -> str: def get_order_id_conditional(self, order: CcxtOrder) -> str:
if order.get("type", "") == "stop": if order.get("type", "") == "stop":
return safe_value_fallback2(order, order, "id_stop", "id") return safe_value_fallback2(order, order, "id_stop", "id")
return order["id"] return order["id"]
def cancel_stoploss_order( def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
self, order_id: str, pair: str, params: Optional[dict] = None
) -> dict:
params1 = {"stop": True} params1 = {"stop": True}
# 'ordType': 'conditional' # 'ordType': 'conditional'
# #
@@ -273,7 +272,7 @@ class Okx(Exchange):
params=params1, params=params1,
) )
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[dict]: def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
orders = [] orders = []
orders = self._api.fetch_closed_orders(pair, since=since_ms) orders = self._api.fetch_closed_orders(pair, since=since_ms)

View File

@@ -2,7 +2,6 @@ import logging
import random import random
from abc import abstractmethod from abc import abstractmethod
from enum import Enum from enum import Enum
from typing import Optional, Union
import gymnasium as gym import gymnasium as gym
import numpy as np import numpy as np
@@ -140,7 +139,7 @@ class BaseEnvironment(gym.Env):
self._end_tick: int = len(self.prices) - 1 self._end_tick: int = len(self.prices) - 1
self._done: bool = False self._done: bool = False
self._current_tick: int = self._start_tick self._current_tick: int = self._start_tick
self._last_trade_tick: Optional[int] = None self._last_trade_tick: int | None = None
self._position = Positions.Neutral self._position = Positions.Neutral
self._position_history: list = [None] self._position_history: list = [None]
self.total_reward: float = 0 self.total_reward: float = 0
@@ -173,8 +172,8 @@ class BaseEnvironment(gym.Env):
def tensorboard_log( def tensorboard_log(
self, self,
metric: str, metric: str,
value: Optional[Union[int, float]] = None, value: int | float | None = None,
inc: Optional[bool] = None, inc: bool | None = None,
category: str = "custom", category: str = "custom",
): ):
""" """

View File

@@ -2,9 +2,10 @@ import copy
import importlib import importlib
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from collections.abc import Callable
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Optional, Union from typing import Any
import gymnasium as gym import gymnasium as gym
import numpy as np import numpy as np
@@ -49,9 +50,9 @@ class BaseReinforcementLearningModel(IFreqaiModel):
) )
th.set_num_threads(self.max_threads) th.set_num_threads(self.max_threads)
self.reward_params = self.freqai_info["rl_config"]["model_reward_parameters"] self.reward_params = self.freqai_info["rl_config"]["model_reward_parameters"]
self.train_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env() self.train_env: VecMonitor | SubprocVecEnv | gym.Env = gym.Env()
self.eval_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env() self.eval_env: VecMonitor | SubprocVecEnv | gym.Env = gym.Env()
self.eval_callback: Optional[MaskableEvalCallback] = None self.eval_callback: MaskableEvalCallback | None = None
self.model_type = self.freqai_info["rl_config"]["model_type"] self.model_type = self.freqai_info["rl_config"]["model_type"]
self.rl_config = self.freqai_info["rl_config"] self.rl_config = self.freqai_info["rl_config"]
self.df_raw: DataFrame = DataFrame() self.df_raw: DataFrame = DataFrame()

View File

@@ -5,7 +5,7 @@ import random
import shutil import shutil
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
@@ -111,7 +111,7 @@ class FreqaiDataKitchen:
def set_paths( def set_paths(
self, self,
pair: str, pair: str,
trained_timestamp: Optional[int] = None, trained_timestamp: int | None = None,
) -> None: ) -> None:
""" """
Set the paths to the data for the present coin/botloop Set the paths to the data for the present coin/botloop

View File

@@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
from collections import deque from collections import deque
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Literal, Optional from typing import Any, Literal
import datasieve.transforms as ds import datasieve.transforms as ds
import numpy as np import numpy as np
@@ -106,7 +106,7 @@ class IFreqaiModel(ABC):
self._threads: list[threading.Thread] = [] self._threads: list[threading.Thread] = []
self._stop_event = threading.Event() self._stop_event = threading.Event()
self.metadata: dict[str, Any] = self.dd.load_global_metadata_from_disk() self.metadata: dict[str, Any] = self.dd.load_global_metadata_from_disk()
self.data_provider: Optional[DataProvider] = None self.data_provider: DataProvider | None = None
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1) self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
self.can_short = True # overridden in start() with strategy.can_short self.can_short = True # overridden in start() with strategy.can_short
self.model: Any = None self.model: Any = None
@@ -185,6 +185,7 @@ class IFreqaiModel(ABC):
Callback for Subclasses to override to include logic for shutting down resources Callback for Subclasses to override to include logic for shutting down resources
when SIGINT is sent. when SIGINT is sent.
""" """
self.dd.save_historic_predictions_to_disk()
return return
def shutdown(self): def shutdown(self):
@@ -198,9 +199,16 @@ class IFreqaiModel(ABC):
self.data_provider = None self.data_provider = None
self._on_stop() self._on_stop()
logger.info("Waiting on Training iteration") if self.freqai_info.get("wait_for_training_iteration_on_reload", True):
for _thread in self._threads: logger.info("Waiting on Training iteration")
_thread.join() for _thread in self._threads:
_thread.join()
else:
logger.warning(
"Breaking current training iteration because "
"you set wait_for_training_iteration_on_reload to "
" False."
)
def start_scanning(self, *args, **kwargs) -> None: def start_scanning(self, *args, **kwargs) -> None:
""" """
@@ -286,7 +294,9 @@ class IFreqaiModel(ABC):
# tr_backtest is the backtesting time range e.g. the week directly # tr_backtest is the backtesting time range e.g. the week directly
# following tr_train. Both of these windows slide through the # following tr_train. Both of these windows slide through the
# entire backtest # entire backtest
for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges): for tr_train, tr_backtest in zip(
dk.training_timeranges, dk.backtesting_timeranges, strict=False
):
(_, _) = self.dd.get_pair_dict_info(pair) (_, _) = self.dd.get_pair_dict_info(pair)
train_it += 1 train_it += 1
total_trains = len(dk.backtesting_timeranges) total_trains = len(dk.backtesting_timeranges)

View File

@@ -1,6 +1,6 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any
import torch as th import torch as th
from stable_baselines3.common.callbacks import ProgressBarCallback from stable_baselines3.common.callbacks import ProgressBarCallback
@@ -78,7 +78,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
model = self.dd.model_dictionary[dk.pair] model = self.dd.model_dictionary[dk.pair]
model.set_env(self.train_env) model.set_env(self.train_env)
callbacks: list[Any] = [self.eval_callback, self.tensorboard_callback] callbacks: list[Any] = [self.eval_callback, self.tensorboard_callback]
progressbar_callback: Optional[ProgressBarCallback] = None progressbar_callback: ProgressBarCallback | None = None
if self.rl_config.get("progress_bar", False): if self.rl_config.get("progress_bar", False):
progressbar_callback = ProgressBarCallback() progressbar_callback = ProgressBarCallback()
callbacks.insert(0, progressbar_callback) callbacks.insert(0, progressbar_callback)

View File

@@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from typing import Any, Union from typing import Any
from stable_baselines3.common.callbacks import BaseCallback from stable_baselines3.common.callbacks import BaseCallback
from stable_baselines3.common.logger import HParam from stable_baselines3.common.logger import HParam
@@ -27,7 +27,7 @@ class TensorboardCallback(BaseCallback):
# "batch_size": self.model.batch_size, # "batch_size": self.model.batch_size,
# "n_steps": self.model.n_steps, # "n_steps": self.model.n_steps,
} }
metric_dict: dict[str, Union[float, int]] = { metric_dict: dict[str, float | int] = {
"eval/mean_reward": 0, "eval/mean_reward": 0,
"rollout/ep_rew_mean": 0, "rollout/ep_rew_mean": 0,
"rollout/ep_len_mean": 0, "rollout/ep_len_mean": 0,

View File

@@ -45,7 +45,7 @@ class TensorBoardCallback(BaseTensorBoardCallback):
return False return False
evals = ["validation", "train"] evals = ["validation", "train"]
for metric, eval_ in zip(evals_log.items(), evals): for metric, eval_ in zip(evals_log.items(), evals, strict=False):
for metric_name, log in metric[1].items(): for metric_name, log in metric[1].items():
score = log[-1][0] if isinstance(log[-1], tuple) else log[-1] score = log[-1][0] if isinstance(log[-1], tuple) else log[-1]
self.writer.add_scalar(f"{eval_}-{metric_name}", score, epoch) self.writer.add_scalar(f"{eval_}-{metric_name}", score, epoch)

View File

@@ -1,6 +1,6 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any
import pandas as pd import pandas as pd
import torch import torch
@@ -50,8 +50,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
self.criterion = criterion self.criterion = criterion
self.model_meta_data = model_meta_data self.model_meta_data = model_meta_data
self.device = device self.device = device
self.n_epochs: Optional[int] = kwargs.get("n_epochs", 10) self.n_epochs: int | None = kwargs.get("n_epochs", 10)
self.n_steps: Optional[int] = kwargs.get("n_steps", None) self.n_steps: int | None = kwargs.get("n_steps", None)
if self.n_steps is None and not self.n_epochs: if self.n_steps is None and not self.n_epochs:
raise Exception("Either `n_steps` or `n_epochs` should be set.") raise Exception("Either `n_steps` or `n_epochs` should be set.")

View File

@@ -107,7 +107,7 @@ def plot_feature_importance(
# Extract feature importance from model # Extract feature importance from model
models = {} models = {}
if "FreqaiMultiOutputRegressor" in str(model.__class__): if "FreqaiMultiOutputRegressor" in str(model.__class__):
for estimator, label in zip(model.estimators_, dk.label_list): for estimator, label in zip(model.estimators_, dk.label_list, strict=False):
models[label] = estimator models[label] = estimator
else: else:
models[dk.label_list[0]] = model models[dk.label_list[0]] = model

View File

@@ -9,7 +9,7 @@ from datetime import datetime, time, timedelta, timezone
from math import isclose from math import isclose
from threading import Lock from threading import Lock
from time import sleep from time import sleep
from typing import Any, Optional from typing import Any
from schedule import Scheduler from schedule import Scheduler
@@ -43,6 +43,7 @@ from freqtrade.exchange import (
timeframe_to_next_date, timeframe_to_next_date,
timeframe_to_seconds, timeframe_to_seconds,
) )
from freqtrade.exchange.exchange_types import CcxtOrder
from freqtrade.leverage.liquidation_price import update_liquidation_prices from freqtrade.leverage.liquidation_price import update_liquidation_prices
from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
@@ -111,7 +112,7 @@ class FreqtradeBot(LoggingMixin):
self.trading_mode: TradingMode = self.config.get("trading_mode", TradingMode.SPOT) self.trading_mode: TradingMode = self.config.get("trading_mode", TradingMode.SPOT)
self.margin_mode: MarginMode = self.config.get("margin_mode", MarginMode.NONE) self.margin_mode: MarginMode = self.config.get("margin_mode", MarginMode.NONE)
self.last_process: Optional[datetime] = None self.last_process: datetime | None = None
# RPC runs in separate threads, can start handling external commands just after # RPC runs in separate threads, can start handling external commands just after
# initialization, even before Freqtradebot has a chance to start its throttling, # initialization, even before Freqtradebot has a chance to start its throttling,
@@ -325,7 +326,7 @@ class FreqtradeBot(LoggingMixin):
} }
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def _refresh_active_whitelist(self, trades: Optional[list[Trade]] = None) -> list[str]: def _refresh_active_whitelist(self, trades: list[Trade] | None = None) -> list[str]:
""" """
Refresh active whitelist from pairlist or edge and extend it with Refresh active whitelist from pairlist or edge and extend it with
pairs that have open trades. pairs that have open trades.
@@ -579,7 +580,7 @@ class FreqtradeBot(LoggingMixin):
logger.warning( logger.warning(
f"{trade} has a total of {trade.amount} {trade.base_currency}, " f"{trade} has a total of {trade.amount} {trade.base_currency}, "
f"but the Wallet shows a total of {total} {trade.base_currency}. " f"but the Wallet shows a total of {total} {trade.base_currency}. "
f"Adjusting trade amount to {total}." f"Adjusting trade amount to {total}. "
"This may however lead to further issues." "This may however lead to further issues."
) )
trade.amount = total trade.amount = total
@@ -587,7 +588,7 @@ class FreqtradeBot(LoggingMixin):
logger.warning( logger.warning(
f"{trade} has a total of {trade.amount} {trade.base_currency}, " f"{trade} has a total of {trade.amount} {trade.base_currency}, "
f"but the Wallet shows a total of {total} {trade.base_currency}. " f"but the Wallet shows a total of {total} {trade.base_currency}. "
"Refusing to adjust as the difference is too large." "Refusing to adjust as the difference is too large. "
"This may however lead to further issues." "This may however lead to further issues."
) )
if prev_trade_amount != trade.amount: if prev_trade_amount != trade.amount:
@@ -862,14 +863,14 @@ class FreqtradeBot(LoggingMixin):
self, self,
pair: str, pair: str,
stake_amount: float, stake_amount: float,
price: Optional[float] = None, price: float | None = None,
*, *,
is_short: bool = False, is_short: bool = False,
ordertype: Optional[str] = None, ordertype: str | None = None,
enter_tag: Optional[str] = None, enter_tag: str | None = None,
trade: Optional[Trade] = None, trade: Trade | None = None,
mode: EntryExecuteMode = "initial", mode: EntryExecuteMode = "initial",
leverage_: Optional[float] = None, leverage_: float | None = None,
) -> bool: ) -> bool:
""" """
Executes an entry for the given pair Executes an entry for the given pair
@@ -1078,13 +1079,13 @@ class FreqtradeBot(LoggingMixin):
def get_valid_enter_price_and_stake( def get_valid_enter_price_and_stake(
self, self,
pair: str, pair: str,
price: Optional[float], price: float | None,
stake_amount: float, stake_amount: float,
trade_side: LongShort, trade_side: LongShort,
entry_tag: Optional[str], entry_tag: str | None,
trade: Optional[Trade], trade: Trade | None,
mode: EntryExecuteMode, mode: EntryExecuteMode,
leverage_: Optional[float], leverage_: float | None,
) -> tuple[float, float, float]: ) -> tuple[float, float, float]:
""" """
Validate and eventually adjust (within limits) limit, amount and leverage Validate and eventually adjust (within limits) limit, amount and leverage
@@ -1180,7 +1181,7 @@ class FreqtradeBot(LoggingMixin):
self, self,
trade: Trade, trade: Trade,
order: Order, order: Order,
order_type: Optional[str], order_type: str | None,
fill: bool = False, fill: bool = False,
sub_trade: bool = False, sub_trade: bool = False,
) -> None: ) -> None:
@@ -1195,6 +1196,13 @@ class FreqtradeBot(LoggingMixin):
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, side="entry", is_short=trade.is_short, refresh=False trade.pair, side="entry", is_short=trade.is_short, refresh=False
) )
stake_amount = trade.stake_amount
if not fill:
# If we have open orders, we need to add the stake amount of the open orders
# as it's not yet included in the trade.stake_amount
stake_amount += sum(
o.stake_amount for o in trade.open_orders if o.ft_order_side == trade.entry_side
)
msg: RPCEntryMsg = { msg: RPCEntryMsg = {
"trade_id": trade.id, "trade_id": trade.id,
@@ -1208,12 +1216,12 @@ class FreqtradeBot(LoggingMixin):
"limit": open_rate, # Deprecated (?) "limit": open_rate, # Deprecated (?)
"open_rate": open_rate, "open_rate": open_rate,
"order_type": order_type or "unknown", "order_type": order_type or "unknown",
"stake_amount": trade.stake_amount, "stake_amount": stake_amount,
"stake_currency": self.config["stake_currency"], "stake_currency": self.config["stake_currency"],
"base_currency": self.exchange.get_pair_base_currency(trade.pair), "base_currency": self.exchange.get_pair_base_currency(trade.pair),
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair), "quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
"fiat_currency": self.config.get("fiat_display_currency", None), "fiat_currency": self.config.get("fiat_display_currency", None),
"amount": order.safe_amount_after_fee if fill else (order.amount or trade.amount), "amount": order.safe_amount_after_fee if fill else (order.safe_amount or trade.amount),
"open_date": trade.open_date_utc or datetime.now(timezone.utc), "open_date": trade.open_date_utc or datetime.now(timezone.utc),
"current_rate": current_rate, "current_rate": current_rate,
"sub_trade": sub_trade, "sub_trade": sub_trade,
@@ -1344,7 +1352,7 @@ class FreqtradeBot(LoggingMixin):
return False return False
def _check_and_execute_exit( def _check_and_execute_exit(
self, trade: Trade, exit_rate: float, enter: bool, exit_: bool, exit_tag: Optional[str] self, trade: Trade, exit_rate: float, enter: bool, exit_: bool, exit_tag: str | None
) -> bool: ) -> bool:
""" """
Check and execute trade exit Check and execute trade exit
@@ -1466,7 +1474,7 @@ class FreqtradeBot(LoggingMixin):
return False return False
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None: def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: CcxtOrder) -> None:
""" """
Check to see if stoploss on exchange should be updated Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange in case of trailing stoploss on exchange
@@ -1504,7 +1512,7 @@ class FreqtradeBot(LoggingMixin):
f"Could not create trailing stoploss order for pair {trade.pair}." f"Could not create trailing stoploss order for pair {trade.pair}."
) )
def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: list[dict]): def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: list[CcxtOrder]):
""" """
Perform required actions according to existing stoploss orders of trade Perform required actions according to existing stoploss orders of trade
:param trade: Corresponding Trade :param trade: Corresponding Trade
@@ -1580,7 +1588,9 @@ class FreqtradeBot(LoggingMixin):
else: else:
self.replace_order(order, open_order, trade) self.replace_order(order, open_order, trade)
def handle_cancel_order(self, order: dict, order_obj: Order, trade: Trade, reason: str) -> None: def handle_cancel_order(
self, order: CcxtOrder, order_obj: Order, trade: Trade, reason: str
) -> None:
""" """
Check if current analyzed order timed out and cancel if necessary. Check if current analyzed order timed out and cancel if necessary.
:param order: Order dict grabbed with exchange.fetch_order() :param order: Order dict grabbed with exchange.fetch_order()
@@ -1602,7 +1612,7 @@ class FreqtradeBot(LoggingMixin):
self.emergency_exit(trade, order["price"], order["amount"]) self.emergency_exit(trade, order["price"], order["amount"])
def emergency_exit( def emergency_exit(
self, trade: Trade, price: float, sub_trade_amt: Optional[float] = None self, trade: Trade, price: float, sub_trade_amt: float | None = None
) -> None: ) -> None:
try: try:
self.execute_trade_exit( self.execute_trade_exit(
@@ -1632,7 +1642,7 @@ class FreqtradeBot(LoggingMixin):
) )
trade.delete() trade.delete()
def replace_order(self, order: dict, order_obj: Optional[Order], trade: Trade) -> None: def replace_order(self, order: CcxtOrder, order_obj: Order | None, trade: Trade) -> None:
""" """
Check if current analyzed entry order should be replaced or simply cancelled. Check if current analyzed entry order should be replaced or simply cancelled.
To simply cancel the existing order(no replacement) adjust_entry_price() should return None To simply cancel the existing order(no replacement) adjust_entry_price() should return None
@@ -1736,10 +1746,10 @@ class FreqtradeBot(LoggingMixin):
def handle_cancel_enter( def handle_cancel_enter(
self, self,
trade: Trade, trade: Trade,
order: dict, order: CcxtOrder,
order_obj: Order, order_obj: Order,
reason: str, reason: str,
replacing: Optional[bool] = False, replacing: bool | None = False,
) -> bool: ) -> bool:
""" """
entry cancel - cancel order entry cancel - cancel order
@@ -1820,7 +1830,9 @@ class FreqtradeBot(LoggingMixin):
) )
return was_trade_fully_canceled return was_trade_fully_canceled
def handle_cancel_exit(self, trade: Trade, order: dict, order_obj: Order, reason: str) -> bool: def handle_cancel_exit(
self, trade: Trade, order: CcxtOrder, order_obj: Order, reason: str
) -> bool:
""" """
exit order cancel - cancel order and update trade exit order cancel - cancel order and update trade
:return: True if exit order was cancelled, false otherwise :return: True if exit order was cancelled, false otherwise
@@ -1931,9 +1943,9 @@ class FreqtradeBot(LoggingMixin):
limit: float, limit: float,
exit_check: ExitCheckTuple, exit_check: ExitCheckTuple,
*, *,
exit_tag: Optional[str] = None, exit_tag: str | None = None,
ordertype: Optional[str] = None, ordertype: str | None = None,
sub_trade_amt: Optional[float] = None, sub_trade_amt: float | None = None,
) -> bool: ) -> bool:
""" """
Executes a trade exit for the given trade and limit Executes a trade exit for the given trade and limit
@@ -2042,10 +2054,10 @@ class FreqtradeBot(LoggingMixin):
def _notify_exit( def _notify_exit(
self, self,
trade: Trade, trade: Trade,
order_type: Optional[str], order_type: str | None,
fill: bool = False, fill: bool = False,
sub_trade: bool = False, sub_trade: bool = False,
order: Optional[Order] = None, order: Order | None = None,
) -> None: ) -> None:
""" """
Sends rpc notification when a sell occurred. Sends rpc notification when a sell occurred.
@@ -2158,7 +2170,7 @@ class FreqtradeBot(LoggingMixin):
# Send the message # Send the message
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def order_obj_or_raise(self, order_id: str, order_obj: Optional[Order]) -> Order: def order_obj_or_raise(self, order_id: str, order_obj: Order | None) -> Order:
if not order_obj: if not order_obj:
raise DependencyException( raise DependencyException(
f"Order_obj not found for {order_id}. This should not have happened." f"Order_obj not found for {order_id}. This should not have happened."
@@ -2172,8 +2184,8 @@ class FreqtradeBot(LoggingMixin):
def update_trade_state( def update_trade_state(
self, self,
trade: Trade, trade: Trade,
order_id: Optional[str], order_id: str | None,
action_order: Optional[dict[str, Any]] = None, action_order: CcxtOrder | None = None,
*, *,
stoploss_order: bool = False, stoploss_order: bool = False,
send_msg: bool = True, send_msg: bool = True,
@@ -2284,7 +2296,7 @@ class FreqtradeBot(LoggingMixin):
def handle_protections(self, pair: str, side: LongShort) -> None: def handle_protections(self, pair: str, side: LongShort) -> None:
# Lock pair for one candle to prevent immediate re-entries # Lock pair for one candle to prevent immediate re-entries
self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason="Auto lock") self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason="Auto lock", side=side)
prot_trig = self.protections.stop_per_pair(pair, side=side) prot_trig = self.protections.stop_per_pair(pair, side=side)
if prot_trig: if prot_trig:
msg: RPCProtectionMsg = { msg: RPCProtectionMsg = {
@@ -2310,7 +2322,7 @@ class FreqtradeBot(LoggingMixin):
amount: float, amount: float,
fee_abs: float, fee_abs: float,
order_obj: Order, order_obj: Order,
) -> Optional[float]: ) -> float | None:
""" """
Applies the fee to amount (either from Order or from Trades). Applies the fee to amount (either from Order or from Trades).
Can eat into dust if more than the required asset is available. Can eat into dust if more than the required asset is available.
@@ -2338,7 +2350,7 @@ class FreqtradeBot(LoggingMixin):
return fee_abs return fee_abs
return None return None
def handle_order_fee(self, trade: Trade, order_obj: Order, order: dict[str, Any]) -> None: def handle_order_fee(self, trade: Trade, order_obj: Order, order: CcxtOrder) -> None:
# Try update amount (binance-fix) # Try update amount (binance-fix)
try: try:
fee_abs = self.get_real_amount(trade, order, order_obj) fee_abs = self.get_real_amount(trade, order, order_obj)
@@ -2347,7 +2359,7 @@ class FreqtradeBot(LoggingMixin):
except DependencyException as exception: except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception) logger.warning("Could not update trade amount: %s", exception)
def get_real_amount(self, trade: Trade, order: dict, order_obj: Order) -> Optional[float]: def get_real_amount(self, trade: Trade, order: CcxtOrder, order_obj: Order) -> float | None:
""" """
Detect and update trade fee. Detect and update trade fee.
Calls trade.update_fee() upon correct detection. Calls trade.update_fee() upon correct detection.
@@ -2407,8 +2419,8 @@ class FreqtradeBot(LoggingMixin):
return True return True
def fee_detection_from_trades( def fee_detection_from_trades(
self, trade: Trade, order: dict, order_obj: Order, order_amount: float, trades: list self, trade: Trade, order: CcxtOrder, order_obj: Order, order_amount: float, trades: list
) -> Optional[float]: ) -> float | None:
""" """
fee-detection fallback to Trades. fee-detection fallback to Trades.
Either uses provided trades list or the result of fetch_my_trades to get correct fee. Either uses provided trades list or the result of fetch_my_trades to get correct fee.

View File

@@ -1,4 +1,4 @@
from typing import Any, Optional from typing import Any
from typing_extensions import TypedDict from typing_extensions import TypedDict
@@ -26,7 +26,7 @@ class BacktestHistoryEntryType(BacktestMetadataType):
filename: str filename: str
strategy: str strategy: str
notes: str notes: str
backtest_start_ts: Optional[int] backtest_start_ts: int | None
backtest_end_ts: Optional[int] backtest_end_ts: int | None
timeframe: Optional[str] timeframe: str | None
timeframe_detail: Optional[str] timeframe_detail: str | None

View File

@@ -1,5 +1,4 @@
# Used for list-exchanges # Used for list-exchanges
from typing import Optional
from typing_extensions import TypedDict from typing_extensions import TypedDict
@@ -17,5 +16,5 @@ class ValidExchangesType(TypedDict):
comment: str comment: str
dex: bool dex: bool
is_alias: bool is_alias: bool
alias_for: Optional[str] alias_for: str | None
trade_modes: list[TradeModeType] trade_modes: list[TradeModeType]

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import Optional
from freqtrade.enums import MarginMode from freqtrade.enums import MarginMode
from freqtrade.exceptions import DependencyException from freqtrade.exceptions import DependencyException
@@ -12,7 +11,7 @@ logger = logging.getLogger(__name__)
def update_liquidation_prices( def update_liquidation_prices(
trade: Optional[LocalTrade] = None, trade: LocalTrade | None = None,
*, *,
exchange: Exchange, exchange: Exchange,
wallets: Wallets, wallets: Wallets,

View File

@@ -6,11 +6,11 @@ Read the documentation to know what cli arguments you need.
import logging import logging
import sys import sys
from typing import Any, Optional from typing import Any
# check min. python version # check min. python version
if sys.version_info < (3, 10): # pragma: no cover if sys.version_info < (3, 10): # pragma: no cover # noqa: UP036
sys.exit("Freqtrade requires Python version >= 3.10") sys.exit("Freqtrade requires Python version >= 3.10")
from freqtrade import __version__ from freqtrade import __version__
@@ -24,7 +24,7 @@ from freqtrade.system import asyncio_setup, gc_set_threshold
logger = logging.getLogger("freqtrade") logger = logging.getLogger("freqtrade")
def main(sysargv: Optional[list[str]] = None) -> None: def main(sysargv: list[str] | None = None) -> None:
""" """
This function will initiate the bot and start the trading loop. This function will initiate the bot and start the trading loop.
:return: None :return: None

View File

@@ -7,7 +7,7 @@ import logging
from collections.abc import Iterator, Mapping from collections.abc import Iterator, Mapping
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
from typing import Any, Optional, TextIO, Union from typing import Any, TextIO
from urllib.parse import urlparse from urllib.parse import urlparse
import pandas as pd import pandas as pd
@@ -129,10 +129,10 @@ def round_dict(d, n):
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()} return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
DictMap = Union[dict[str, Any], Mapping[str, Any]] DictMap = dict[str, Any] | Mapping[str, Any]
def safe_value_fallback(obj: DictMap, key1: str, key2: Optional[str] = None, default_value=None): def safe_value_fallback(obj: DictMap, key1: str, key2: str | None = None, default_value=None):
""" """
Search a value in obj, return this if it's not None. Search a value in obj, return this if it's not None.
Then search key2 in obj - return that if it's not none - then use default_value. Then search key2 in obj - return that if it's not none - then use default_value.
@@ -161,7 +161,7 @@ def safe_value_fallback2(dict1: DictMap, dict2: DictMap, key1: str, key2: str, d
return default_value return default_value
def plural(num: float, singular: str, plural: Optional[str] = None) -> str: def plural(num: float, singular: str, plural: str | None = None) -> str:
return singular if (num == 1 or num == -1) else plural or singular + "s" return singular if (num == 1 or num == -1) else plural or singular + "s"

View File

@@ -1,4 +1,4 @@
from typing import Callable from collections.abc import Callable
from cachetools import TTLCache, cached from cachetools import TTLCache, cached

View File

@@ -1,7 +1,7 @@
import logging import logging
import time import time
from pathlib import Path from pathlib import Path
from typing import Any, Union from typing import Any
import pandas as pd import pandas as pd
from rich.text import Text from rich.text import Text
@@ -21,7 +21,7 @@ class LookaheadAnalysisSubFunctions:
def text_table_lookahead_analysis_instances( def text_table_lookahead_analysis_instances(
config: dict[str, Any], config: dict[str, Any],
lookahead_instances: list[LookaheadAnalysis], lookahead_instances: list[LookaheadAnalysis],
caption: Union[str, None] = None, caption: str | None = None,
): ):
headers = [ headers = [
"filename", "filename",
@@ -243,7 +243,7 @@ class LookaheadAnalysisSubFunctions:
# report the results # report the results
if lookaheadAnalysis_instances: if lookaheadAnalysis_instances:
caption: Union[str, None] = None caption: str | None = None
if any( if any(
[ [
any( any(

View File

@@ -1,7 +1,6 @@
import hashlib import hashlib
from copy import deepcopy from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Union
import rapidjson import rapidjson
@@ -38,7 +37,7 @@ def get_strategy_run_id(strategy) -> str:
return digest.hexdigest().lower() return digest.hexdigest().lower()
def get_backtest_metadata_filename(filename: Union[Path, str]) -> Path: def get_backtest_metadata_filename(filename: Path | str) -> Path:
"""Return metadata filename for specified backtest results file.""" """Return metadata filename for specified backtest results file."""
filename = Path(filename) filename = Path(filename)
return filename.parent / Path(f"{filename.stem}.meta{filename.suffix}") return filename.parent / Path(f"{filename.stem}.meta{filename.suffix}")

View File

@@ -8,7 +8,7 @@ import logging
from collections import defaultdict from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any, Optional from typing import Any
from numpy import nan from numpy import nan
from pandas import DataFrame from pandas import DataFrame
@@ -110,7 +110,7 @@ class Backtesting:
backtesting.start() backtesting.start()
""" """
def __init__(self, config: Config, exchange: Optional[Exchange] = None) -> None: def __init__(self, config: Config, exchange: Exchange | None = None) -> None:
LoggingMixin.show_output = False LoggingMixin.show_output = False
self.config = config self.config = config
self.results: BacktestResultType = get_BacktestResultType_default() self.results: BacktestResultType = get_BacktestResultType_default()
@@ -685,7 +685,7 @@ class Backtesting:
) )
def _try_close_open_order( def _try_close_open_order(
self, order: Optional[Order], trade: LocalTrade, current_date: datetime, row: tuple self, order: Order | None, trade: LocalTrade, current_date: datetime, row: tuple
) -> bool: ) -> bool:
""" """
Check if an order is open and if it should've filled. Check if an order is open and if it should've filled.
@@ -732,7 +732,6 @@ class Backtesting:
trade.close_date = current_time trade.close_date = current_time
trade.close(order.ft_price, show_msg=False) trade.close(order.ft_price, show_msg=False)
# logger.debug(f"{pair} - Backtesting exit {trade}")
LocalTrade.close_bt_trade(trade) LocalTrade.close_bt_trade(trade)
self.wallets.update() self.wallets.update()
self.run_protections(pair, current_time, trade.trade_direction) self.run_protections(pair, current_time, trade.trade_direction)
@@ -743,8 +742,8 @@ class Backtesting:
row: tuple, row: tuple,
exit_: ExitCheckTuple, exit_: ExitCheckTuple,
current_time: datetime, current_time: datetime,
amount: Optional[float] = None, amount: float | None = None,
) -> Optional[LocalTrade]: ) -> LocalTrade | None:
if exit_.exit_flag: if exit_.exit_flag:
trade.close_date = current_time trade.close_date = current_time
exit_reason = exit_.exit_reason exit_reason = exit_.exit_reason
@@ -823,8 +822,8 @@ class Backtesting:
sell_row: tuple, sell_row: tuple,
close_rate: float, close_rate: float,
amount: float, amount: float,
exit_reason: Optional[str], exit_reason: str | None,
) -> Optional[LocalTrade]: ) -> LocalTrade | None:
self.order_id_counter += 1 self.order_id_counter += 1
exit_candle_time = sell_row[DATE_IDX].to_pydatetime() exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
order_type = self.strategy.order_types["exit"] order_type = self.strategy.order_types["exit"]
@@ -860,7 +859,7 @@ class Backtesting:
def _check_trade_exit( def _check_trade_exit(
self, trade: LocalTrade, row: tuple, current_time: datetime self, trade: LocalTrade, row: tuple, current_time: datetime
) -> Optional[LocalTrade]: ) -> LocalTrade | None:
self._run_funding_fees(trade, current_time) self._run_funding_fees(trade, current_time)
# Check if we need to adjust our current positions # Check if we need to adjust our current positions
@@ -910,10 +909,10 @@ class Backtesting:
stake_amount: float, stake_amount: float,
direction: LongShort, direction: LongShort,
current_time: datetime, current_time: datetime,
entry_tag: Optional[str], entry_tag: str | None,
trade: Optional[LocalTrade], trade: LocalTrade | None,
order_type: str, order_type: str,
price_precision: Optional[float], price_precision: float | None,
) -> tuple[float, float, float, float]: ) -> tuple[float, float, float, float]:
if order_type == "limit": if order_type == "limit":
new_rate = strategy_safe_wrapper( new_rate = strategy_safe_wrapper(
@@ -1005,12 +1004,12 @@ class Backtesting:
pair: str, pair: str,
row: tuple, row: tuple,
direction: LongShort, direction: LongShort,
stake_amount: Optional[float] = None, stake_amount: float | None = None,
trade: Optional[LocalTrade] = None, trade: LocalTrade | None = None,
requested_rate: Optional[float] = None, requested_rate: float | None = None,
requested_stake: Optional[float] = None, requested_stake: float | None = None,
entry_tag1: Optional[str] = None, entry_tag1: str | None = None,
) -> Optional[LocalTrade]: ) -> LocalTrade | None:
""" """
:param trade: Trade to adjust - initial entry if None :param trade: Trade to adjust - initial entry if None
:param requested_rate: Adjusted entry rate :param requested_rate: Adjusted entry rate
@@ -1103,6 +1102,7 @@ class Backtesting:
fee_close=self.fee, fee_close=self.fee,
is_open=True, is_open=True,
enter_tag=entry_tag, enter_tag=entry_tag,
timeframe=self.timeframe_min,
exchange=self._exchange_name, exchange=self._exchange_name,
is_short=is_short, is_short=is_short,
trading_mode=self.trading_mode, trading_mode=self.trading_mode,
@@ -1178,7 +1178,7 @@ class Backtesting:
self.rejected_trades += 1 self.rejected_trades += 1
return False return False
def check_for_trade_entry(self, row) -> Optional[LongShort]: def check_for_trade_entry(self, row) -> LongShort | None:
enter_long = row[LONG_IDX] == 1 enter_long = row[LONG_IDX] == 1
exit_long = row[ELONG_IDX] == 1 exit_long = row[ELONG_IDX] == 1
enter_short = self._can_short and row[SHORT_IDX] == 1 enter_short = self._can_short and row[SHORT_IDX] == 1
@@ -1216,7 +1216,7 @@ class Backtesting:
def check_order_cancel( def check_order_cancel(
self, trade: LocalTrade, order: Order, current_time: datetime self, trade: LocalTrade, order: Order, current_time: datetime
) -> Optional[bool]: ) -> bool | None:
""" """
Check if current analyzed order has to be canceled. Check if current analyzed order has to be canceled.
Returns True if the trade should be Deleted (initial order was canceled), Returns True if the trade should be Deleted (initial order was canceled),
@@ -1298,7 +1298,7 @@ class Backtesting:
def validate_row( def validate_row(
self, data: dict, pair: str, row_index: int, current_time: datetime self, data: dict, pair: str, row_index: int, current_time: datetime
) -> Optional[tuple]: ) -> tuple | None:
try: try:
# Row is treated as "current incomplete candle". # Row is treated as "current incomplete candle".
# entry / exit signals are shifted by 1 to compensate for this. # entry / exit signals are shifted by 1 to compensate for this.
@@ -1332,14 +1332,41 @@ class Backtesting:
row: tuple, row: tuple,
pair: str, pair: str,
current_time: datetime, current_time: datetime,
trade_dir: Optional[LongShort], trade_dir: LongShort | None,
can_enter: bool, can_enter: bool,
) -> None: ) -> None:
"""
Conditionally call backtest_loop_inner a 2nd time if shorting is enabled,
a position closed and a new signal in the other direction is available.
"""
if not self._can_short or trade_dir is None:
# No need to reverse position if shorting is disabled or there's no new signal
self.backtest_loop_inner(row, pair, current_time, trade_dir, can_enter)
else:
for _ in (0, 1):
a = self.backtest_loop_inner(row, pair, current_time, trade_dir, can_enter)
if not a or a == trade_dir:
# the trade didn't close or position change is in the same direction
break
def backtest_loop_inner(
self,
row: tuple,
pair: str,
current_time: datetime,
trade_dir: LongShort | None,
can_enter: bool,
) -> LongShort | None:
""" """
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized. NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
Backtesting processing for one candle/pair. Backtesting processing for one candle/pair.
""" """
exiting_dir: LongShort | None = None
if not self._position_stacking and len(LocalTrade.bt_trades_open_pp[pair]) > 0:
# position_stacking not supported for now.
exiting_dir = "short" if LocalTrade.bt_trades_open_pp[pair][0].is_short else "long"
for t in list(LocalTrade.bt_trades_open_pp[pair]): for t in list(LocalTrade.bt_trades_open_pp[pair]):
# 1. Manage currently open orders of active trades # 1. Manage currently open orders of active trades
if self.manage_open_orders(t, current_time, row): if self.manage_open_orders(t, current_time, row):
@@ -1358,7 +1385,7 @@ class Backtesting:
and (self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0) and (self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0)
and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir) and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir)
): ):
if self.trade_slot_available(LocalTrade.bt_open_open_trade_count): if self.trade_slot_available(LocalTrade.bt_open_open_trade_count_candle):
trade = self._enter_trade(pair, row, trade_dir) trade = self._enter_trade(pair, row, trade_dir)
if trade: if trade:
self.wallets.update() self.wallets.update()
@@ -1380,6 +1407,10 @@ class Backtesting:
if order: if order:
self._process_exit_order(order, trade, current_time, row, pair) self._process_exit_order(order, trade, current_time, row, pair)
if exiting_dir and len(LocalTrade.bt_trades_open_pp[pair]) == 0:
return exiting_dir
return None
def time_pair_generator( def time_pair_generator(
self, start_date: datetime, end_date: datetime, increment: timedelta, pairs: list[str] self, start_date: datetime, end_date: datetime, increment: timedelta, pairs: list[str]
): ):
@@ -1432,6 +1463,10 @@ class Backtesting:
): ):
if is_first_call: if is_first_call:
self.check_abort() self.check_abort()
# Reset open trade count for this candle
# Critical to avoid exceeding max_open_trades in backtesting
# when timeframe-detail is used and trades close within the opening candle.
LocalTrade.bt_open_open_trade_count_candle = LocalTrade.bt_open_open_trade_count
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)( strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
current_time=current_time current_time=current_time
) )
@@ -1446,7 +1481,7 @@ class Backtesting:
self.dataprovider._set_dataframe_max_index(self.required_startup + row_index) self.dataprovider._set_dataframe_max_index(self.required_startup + row_index)
self.dataprovider._set_dataframe_max_date(current_time) self.dataprovider._set_dataframe_max_date(current_time)
current_detail_time: datetime = row[DATE_IDX].to_pydatetime() current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row) trade_dir: LongShort | None = self.check_for_trade_entry(row)
if ( if (
(trade_dir is not None or len(LocalTrade.bt_trades_open_pp[pair]) > 0) (trade_dir is not None or len(LocalTrade.bt_trades_open_pp[pair]) > 0)
@@ -1517,12 +1552,6 @@ class Backtesting:
backtest_start_time = datetime.now(timezone.utc) backtest_start_time = datetime.now(timezone.utc)
self._set_strategy(strat) self._set_strategy(strat)
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
if not self.config.get("use_max_market_positions", True):
logger.info("Ignoring max_open_trades (--disable-max-market-positions was used) ...")
self.strategy.max_open_trades = float("inf")
self.config.update({"max_open_trades": self.strategy.max_open_trades})
# need to reprocess data every time to populate signals # need to reprocess data every time to populate signals
preprocessed = self.strategy.advise_all_indicators(data) preprocessed = self.strategy.advise_all_indicators(data)

View File

@@ -1,7 +1,7 @@
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Optional from typing import Any
from pandas import DataFrame from pandas import DataFrame
@@ -28,7 +28,7 @@ class BaseAnalysis:
def __init__(self, config: dict[str, Any], strategy_obj: dict): def __init__(self, config: dict[str, Any], strategy_obj: dict):
self.failed_bias_check = True self.failed_bias_check = True
self.full_varHolder = VarHolder() self.full_varHolder = VarHolder()
self.exchange: Optional[Any] = None self.exchange: Any | None = None
self._fee = None self._fee = None
# pull variables the scope of the lookahead_analysis-instance # pull variables the scope of the lookahead_analysis-instance

View File

@@ -11,7 +11,7 @@ import warnings
from datetime import datetime, timezone from datetime import datetime, timezone
from math import ceil from math import ceil
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any
import rapidjson import rapidjson
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
@@ -125,13 +125,7 @@ class Hyperopt:
self.market_change = 0.0 self.market_change = 0.0
self.num_epochs_saved = 0 self.num_epochs_saved = 0
self.current_best_epoch: Optional[dict[str, Any]] = None self.current_best_epoch: dict[str, Any] | None = None
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if not self.config.get("use_max_market_positions", True):
logger.debug("Ignoring max_open_trades (--disable-max-market-positions was used) ...")
self.backtesting.strategy.max_open_trades = float("inf")
config.update({"max_open_trades": self.backtesting.strategy.max_open_trades})
if HyperoptTools.has_space(self.config, "sell"): if HyperoptTools.has_space(self.config, "sell"):
# Make sure use_exit_signal is enabled # Make sure use_exit_signal is enabled
@@ -177,7 +171,7 @@ class Hyperopt:
# Return a dict where the keys are the names of the dimensions # Return a dict where the keys are the names of the dimensions
# and the values are taken from the list of parameters. # and the values are taken from the list of parameters.
return {d.name: v for d, v in zip(dimensions, raw_params)} return {d.name: v for d, v in zip(dimensions, raw_params, strict=False)}
def _save_result(self, epoch: dict) -> None: def _save_result(self, epoch: dict) -> None:
""" """
@@ -485,7 +479,7 @@ class Hyperopt:
delayed(wrap_non_picklable_objects(self.generate_optimizer))(v) for v in asked delayed(wrap_non_picklable_objects(self.generate_optimizer))(v) for v in asked
) )
def _set_random_state(self, random_state: Optional[int]) -> int: def _set_random_state(self, random_state: int | None) -> int:
return random_state or random.randint(1, 2**16 - 1) # noqa: S311 return random_state or random.randint(1, 2**16 - 1) # noqa: S311
def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]: def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]:
@@ -557,7 +551,7 @@ class Hyperopt:
is_random = [True for _ in range(len(asked))] is_random = [True for _ in range(len(asked))]
is_random_non_tried += [ is_random_non_tried += [
rand rand
for x, rand in zip(asked, is_random) for x, rand in zip(asked, is_random, strict=False)
if x not in self.opt.Xi and x not in asked_non_tried if x not in self.opt.Xi and x not in asked_non_tried
] ]
asked_non_tried += [ asked_non_tried += [

View File

@@ -5,8 +5,8 @@ This module implements a convenience auto-hyperopt class, which can be used toge
""" """
import logging import logging
from collections.abc import Callable
from contextlib import suppress from contextlib import suppress
from typing import Callable
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException

View File

@@ -6,7 +6,7 @@ This module defines the interface to apply for hyperopt
import logging import logging
import math import math
from abc import ABC from abc import ABC
from typing import Union from typing import TypeAlias
from sklearn.base import RegressorMixin from sklearn.base import RegressorMixin
from skopt.space import Categorical, Dimension, Integer from skopt.space import Categorical, Dimension, Integer
@@ -20,7 +20,7 @@ from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
EstimatorType = Union[RegressorMixin, str] EstimatorType: TypeAlias = RegressorMixin | str
class IHyperOpt(ABC): class IHyperOpt(ABC):

View File

@@ -1,6 +1,6 @@
import sys import sys
from os import get_terminal_size from os import get_terminal_size
from typing import Any, Optional from typing import Any
from rich.align import Align from rich.align import Align
from rich.console import Console from rich.console import Console
@@ -37,7 +37,7 @@ class HyperoptOutput:
self.table.add_column("Objective", justify="right") self.table.add_column("Objective", justify="right")
self.table.add_column("Max Drawdown (Acct)", justify="right") self.table.add_column("Max Drawdown (Acct)", justify="right")
def print(self, console: Optional[Console] = None, *, print_colorized=True): def print(self, console: Console | None = None, *, print_colorized=True):
if not console: if not console:
console = Console( console = Console(
color_system="auto" if print_colorized else None, color_system="auto" if print_colorized else None,
@@ -57,7 +57,7 @@ class HyperoptOutput:
stake_currency = config["stake_currency"] stake_currency = config["stake_currency"]
self._results.extend(results) self._results.extend(results)
max_rows: Optional[int] = None max_rows: int | None = None
if self._streaming: if self._streaming:
try: try:

View File

@@ -3,7 +3,7 @@ from collections.abc import Iterator
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any
import numpy as np import numpy as np
import rapidjson import rapidjson
@@ -44,7 +44,7 @@ class HyperoptStateContainer:
class HyperoptTools: class HyperoptTools:
@staticmethod @staticmethod
def get_strategy_filename(config: Config, strategy_name: str) -> Optional[Path]: def get_strategy_filename(config: Config, strategy_name: str) -> Path | None:
""" """
Get Strategy-location (filename) from strategy_name Get Strategy-location (filename) from strategy_name
""" """
@@ -188,7 +188,7 @@ class HyperoptTools:
total_epochs: int, total_epochs: int,
print_json: bool, print_json: bool,
no_header: bool = False, no_header: bool = False,
header_str: Optional[str] = None, header_str: str | None = None,
) -> None: ) -> None:
""" """
Display details of the hyperopt result Display details of the hyperopt result
@@ -257,7 +257,7 @@ class HyperoptTools:
@staticmethod @staticmethod
def _params_pretty_print( def _params_pretty_print(
params, space: str, header: str, non_optimized: Optional[dict] = None params, space: str, header: str, non_optimized: dict | None = None
) -> None: ) -> None:
if space in params or (non_optimized and space in non_optimized): if space in params or (non_optimized and space in non_optimized):
space_params = HyperoptTools._space_params(params, space, 5) space_params = HyperoptTools._space_params(params, space, 5)
@@ -299,7 +299,7 @@ class HyperoptTools:
print(result) print(result)
@staticmethod @staticmethod
def _space_params(params, space: str, r: Optional[int] = None) -> dict: def _space_params(params, space: str, r: int | None = None) -> dict:
d = params.get(space) d = params.get(space)
if d: if d:
# Round floats to `r` digits after the decimal point if requested # Round floats to `r` digits after the decimal point if requested

View File

@@ -1,5 +1,5 @@
import logging import logging
from typing import Any, Literal, Union from typing import Any, Literal
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
from freqtrade.ft_types import BacktestResultType from freqtrade.ft_types import BacktestResultType
@@ -18,7 +18,7 @@ def _get_line_floatfmt(stake_currency: str) -> list[str]:
def _get_line_header( def _get_line_header(
first_column: Union[str, list[str]], stake_currency: str, direction: str = "Trades" first_column: str | list[str], stake_currency: str, direction: str = "Trades"
) -> list[str]: ) -> list[str]:
""" """
Generate header lines (goes in line with _generate_result_line()) Generate header lines (goes in line with _generate_result_line())
@@ -172,7 +172,7 @@ def text_table_strategy(strategy_results, stake_currency: str, title: str):
dd_pad_per = max([len(dd) for dd in drawdown]) dd_pad_per = max([len(dd) for dd in drawdown])
drawdown = [ drawdown = [
f'{t["max_drawdown_abs"]:>{dd_pad_abs}} {stake_currency} {dd:>{dd_pad_per}}%' f'{t["max_drawdown_abs"]:>{dd_pad_abs}} {stake_currency} {dd:>{dd_pad_per}}%'
for t, dd in zip(strategy_results, drawdown) for t, dd in zip(strategy_results, drawdown, strict=False)
] ]
output = [ output = [
@@ -186,7 +186,7 @@ def text_table_strategy(strategy_results, stake_currency: str, title: str):
generate_wins_draws_losses(t["wins"], t["draws"], t["losses"]), generate_wins_draws_losses(t["wins"], t["draws"], t["losses"]),
drawdown, drawdown,
] ]
for t, drawdown in zip(strategy_results, drawdown) for t, drawdown in zip(strategy_results, drawdown, strict=False)
] ]
print_rich_table(output, headers, summary=title) print_rich_table(output, headers, summary=title)

View File

@@ -1,6 +1,5 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional
from pandas import DataFrame from pandas import DataFrame
@@ -35,7 +34,7 @@ def store_backtest_stats(
stats: BacktestResultType, stats: BacktestResultType,
dtappendix: str, dtappendix: str,
*, *,
market_change_data: Optional[DataFrame] = None, market_change_data: DataFrame | None = None,
) -> Path: ) -> Path:
""" """
Stores backtest results Stores backtest results

View File

@@ -1,7 +1,7 @@
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any, Literal, Union from typing import Any, Literal
import numpy as np import numpy as np
from pandas import DataFrame, Series, concat, to_datetime from pandas import DataFrame, Series, concat, to_datetime
@@ -69,7 +69,7 @@ def generate_rejected_signals(
def _generate_result_line( def _generate_result_line(
result: DataFrame, starting_balance: int, first_column: Union[str, list[str]] result: DataFrame, starting_balance: int, first_column: str | list[str]
) -> dict: ) -> dict:
""" """
Generate one result dict, with "first_column" as key. Generate one result dict, with "first_column" as key.
@@ -143,7 +143,7 @@ def generate_pair_metrics(
def generate_tag_metrics( def generate_tag_metrics(
tag_type: Union[Literal["enter_tag", "exit_reason"], list[Literal["enter_tag", "exit_reason"]]], tag_type: Literal["enter_tag", "exit_reason"] | list[Literal["enter_tag", "exit_reason"]],
starting_balance: int, starting_balance: int,
results: DataFrame, results: DataFrame,
skip_nan: bool = False, skip_nan: bool = False,
@@ -208,7 +208,7 @@ def _get_resample_from_period(period: str) -> str:
def generate_periodic_breakdown_stats( def generate_periodic_breakdown_stats(
trade_list: Union[list, DataFrame], period: str trade_list: list | DataFrame, period: str
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
results = trade_list if not isinstance(trade_list, list) else DataFrame.from_records(trade_list) results = trade_list if not isinstance(trade_list, list) else DataFrame.from_records(trade_list)
if len(results) == 0: if len(results) == 0:
@@ -559,7 +559,7 @@ def generate_strategy_stats(
def generate_backtest_stats( def generate_backtest_stats(
btdata: dict[str, DataFrame], btdata: dict[str, DataFrame],
all_results: dict[str, dict[str, Union[DataFrame, dict]]], all_results: dict[str, dict[str, DataFrame | dict]],
min_date: datetime, min_date: datetime,
max_date: datetime, max_date: datetime,
) -> BacktestResultType: ) -> BacktestResultType:

View File

@@ -2,7 +2,7 @@ import json
import logging import logging
from collections.abc import Sequence from collections.abc import Sequence
from datetime import datetime from datetime import datetime
from typing import Any, ClassVar, Optional from typing import Any, ClassVar
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, select from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, select
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -42,7 +42,7 @@ class _CustomData(ModelBase):
cd_type: Mapped[str] = mapped_column(String(25), nullable=False) cd_type: Mapped[str] = mapped_column(String(25), nullable=False)
cd_value: Mapped[str] = mapped_column(Text, nullable=False) cd_value: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=dt_now) created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=dt_now)
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
# Empty container value - not persisted, but filled with cd_value on query # Empty container value - not persisted, but filled with cd_value on query
value: Any = None value: Any = None
@@ -62,7 +62,7 @@ class _CustomData(ModelBase):
@classmethod @classmethod
def query_cd( def query_cd(
cls, key: Optional[str] = None, trade_id: Optional[int] = None cls, key: str | None = None, trade_id: int | None = None
) -> Sequence["_CustomData"]: ) -> Sequence["_CustomData"]:
""" """
Get all CustomData, if trade_id is not specified Get all CustomData, if trade_id is not specified
@@ -117,7 +117,7 @@ class CustomDataWrapper:
_CustomData.session.commit() _CustomData.session.commit()
@staticmethod @staticmethod
def get_custom_data(*, trade_id: int, key: Optional[str] = None) -> list[_CustomData]: def get_custom_data(*, trade_id: int, key: str | None = None) -> list[_CustomData]:
if CustomDataWrapper.use_db: if CustomDataWrapper.use_db:
filters = [ filters = [
_CustomData.ft_trade_id == trade_id, _CustomData.ft_trade_id == trade_id,

View File

@@ -1,6 +1,6 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum from enum import Enum
from typing import ClassVar, Optional, Union from typing import ClassVar
from sqlalchemy import String from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from freqtrade.persistence.base import ModelBase, SessionType from freqtrade.persistence.base import ModelBase, SessionType
ValueTypes = Union[str, datetime, float, int] ValueTypes = str | datetime | float | int
class ValueTypesEnum(str, Enum): class ValueTypesEnum(str, Enum):
@@ -37,10 +37,10 @@ class _KeyValueStoreModel(ModelBase):
value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False) value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False)
string_value: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) string_value: Mapped[str | None] = mapped_column(String(255), nullable=True)
datetime_value: Mapped[Optional[datetime]] datetime_value: Mapped[datetime | None]
float_value: Mapped[Optional[float]] float_value: Mapped[float | None]
int_value: Mapped[Optional[int]] int_value: Mapped[int | None]
class KeyValueStore: class KeyValueStore:
@@ -97,7 +97,7 @@ class KeyValueStore:
_KeyValueStoreModel.session.commit() _KeyValueStoreModel.session.commit()
@staticmethod @staticmethod
def get_value(key: KeyStoreKeys) -> Optional[ValueTypes]: def get_value(key: KeyStoreKeys) -> ValueTypes | None:
""" """
Get the value for the given key. Get the value for the given key.
:param key: Key to get the value for :param key: Key to get the value for
@@ -121,7 +121,7 @@ class KeyValueStore:
raise ValueError(f"Unknown value type {kv.value_type}") # pragma: no cover raise ValueError(f"Unknown value type {kv.value_type}") # pragma: no cover
@staticmethod @staticmethod
def get_string_value(key: KeyStoreKeys) -> Optional[str]: def get_string_value(key: KeyStoreKeys) -> str | None:
""" """
Get the value for the given key. Get the value for the given key.
:param key: Key to get the value for :param key: Key to get the value for
@@ -139,7 +139,7 @@ class KeyValueStore:
return kv.string_value return kv.string_value
@staticmethod @staticmethod
def get_datetime_value(key: KeyStoreKeys) -> Optional[datetime]: def get_datetime_value(key: KeyStoreKeys) -> datetime | None:
""" """
Get the value for the given key. Get the value for the given key.
:param key: Key to get the value for :param key: Key to get the value for
@@ -157,7 +157,7 @@ class KeyValueStore:
return kv.datetime_value.replace(tzinfo=timezone.utc) return kv.datetime_value.replace(tzinfo=timezone.utc)
@staticmethod @staticmethod
def get_float_value(key: KeyStoreKeys) -> Optional[float]: def get_float_value(key: KeyStoreKeys) -> float | None:
""" """
Get the value for the given key. Get the value for the given key.
:param key: Key to get the value for :param key: Key to get the value for
@@ -175,7 +175,7 @@ class KeyValueStore:
return kv.float_value return kv.float_value
@staticmethod @staticmethod
def get_int_value(key: KeyStoreKeys) -> Optional[int]: def get_int_value(key: KeyStoreKeys) -> int | None:
""" """
Get the value for the given key. Get the value for the given key.
:param key: Key to get the value for :param key: Key to get the value for

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import Optional
from sqlalchemy import inspect, select, text, update from sqlalchemy import inspect, select, text, update
@@ -32,8 +31,8 @@ def get_backup_name(tabs: list[str], backup_prefix: str):
def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str): def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str):
order_id: Optional[int] = None order_id: int | None = None
trade_id: Optional[int] = None trade_id: int | None = None
if engine.name == "postgresql": if engine.name == "postgresql":
with engine.begin() as connection: with engine.begin() as connection:

View File

@@ -5,7 +5,7 @@ This module contains the class to persist trades into SQLite
import logging import logging
import threading import threading
from contextvars import ContextVar from contextvars import ContextVar
from typing import Any, Final, Optional from typing import Any, Final
from sqlalchemy import create_engine, inspect from sqlalchemy import create_engine, inspect
from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.exc import NoSuchModuleError
@@ -25,10 +25,10 @@ logger = logging.getLogger(__name__)
REQUEST_ID_CTX_KEY: Final[str] = "request_id" REQUEST_ID_CTX_KEY: Final[str] = "request_id"
_request_id_ctx_var: ContextVar[Optional[str]] = ContextVar(REQUEST_ID_CTX_KEY, default=None) _request_id_ctx_var: ContextVar[str | None] = ContextVar(REQUEST_ID_CTX_KEY, default=None)
def get_request_or_thread_id() -> Optional[str]: def get_request_or_thread_id() -> str | None:
""" """
Helper method to get either async context (for fastapi requests), or thread id Helper method to get either async context (for fastapi requests), or thread id
""" """

View File

@@ -1,5 +1,5 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, ClassVar, Optional from typing import Any, ClassVar
from sqlalchemy import ScalarResult, String, or_, select from sqlalchemy import ScalarResult, String, or_, select
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
@@ -21,7 +21,7 @@ class PairLock(ModelBase):
pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True)
# lock direction - long, short or * (for both) # lock direction - long, short or * (for both)
side: Mapped[str] = mapped_column(String(25), nullable=False, default="*") side: Mapped[str] = mapped_column(String(25), nullable=False, default="*")
reason: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
# Time the pair was locked (start time) # Time the pair was locked (start time)
lock_time: Mapped[datetime] = mapped_column(nullable=False) lock_time: Mapped[datetime] = mapped_column(nullable=False)
# Time until the pair is locked (end time) # Time until the pair is locked (end time)
@@ -39,7 +39,7 @@ class PairLock(ModelBase):
@staticmethod @staticmethod
def query_pair_locks( def query_pair_locks(
pair: Optional[str], now: datetime, side: str = "*" pair: str | None, now: datetime, side: str = "*"
) -> ScalarResult["PairLock"]: ) -> ScalarResult["PairLock"]:
""" """
Get all currently active locks for this pair Get all currently active locks for this pair

View File

@@ -1,7 +1,6 @@
import logging import logging
from collections.abc import Sequence from collections.abc import Sequence
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import select from sqlalchemy import select
@@ -36,9 +35,9 @@ class PairLocks:
def lock_pair( def lock_pair(
pair: str, pair: str,
until: datetime, until: datetime,
reason: Optional[str] = None, reason: str | None = None,
*, *,
now: Optional[datetime] = None, now: datetime | None = None,
side: str = "*", side: str = "*",
) -> PairLock: ) -> PairLock:
""" """
@@ -68,7 +67,7 @@ class PairLocks:
@staticmethod @staticmethod
def get_pair_locks( def get_pair_locks(
pair: Optional[str], now: Optional[datetime] = None, side: str = "*" pair: str | None, now: datetime | None = None, side: str = "*"
) -> Sequence[PairLock]: ) -> Sequence[PairLock]:
""" """
Get all currently active locks for this pair Get all currently active locks for this pair
@@ -96,8 +95,8 @@ class PairLocks:
@staticmethod @staticmethod
def get_pair_longest_lock( def get_pair_longest_lock(
pair: str, now: Optional[datetime] = None, side: str = "*" pair: str, now: datetime | None = None, side: str = "*"
) -> Optional[PairLock]: ) -> PairLock | None:
""" """
Get the lock that expires the latest for the pair given. Get the lock that expires the latest for the pair given.
""" """
@@ -106,7 +105,7 @@ class PairLocks:
return locks[0] if locks else None return locks[0] if locks else None
@staticmethod @staticmethod
def unlock_pair(pair: str, now: Optional[datetime] = None, side: str = "*") -> None: def unlock_pair(pair: str, now: datetime | None = None, side: str = "*") -> None:
""" """
Release all locks for this pair. Release all locks for this pair.
:param pair: Pair to unlock :param pair: Pair to unlock
@@ -124,7 +123,7 @@ class PairLocks:
PairLock.session.commit() PairLock.session.commit()
@staticmethod @staticmethod
def unlock_reason(reason: str, now: Optional[datetime] = None) -> None: def unlock_reason(reason: str, now: datetime | None = None) -> None:
""" """
Release all locks for this reason. Release all locks for this reason.
:param reason: Which reason to unlock :param reason: Which reason to unlock
@@ -155,7 +154,7 @@ class PairLocks:
lock.active = False lock.active = False
@staticmethod @staticmethod
def is_global_lock(now: Optional[datetime] = None, side: str = "*") -> bool: def is_global_lock(now: datetime | None = None, side: str = "*") -> bool:
""" """
:param now: Datetime object (generated via datetime.now(timezone.utc)). :param now: Datetime object (generated via datetime.now(timezone.utc)).
defaults to datetime.now(timezone.utc) defaults to datetime.now(timezone.utc)
@@ -166,7 +165,7 @@ class PairLocks:
return len(PairLocks.get_pair_locks("*", now, side)) > 0 return len(PairLocks.get_pair_locks("*", now, side)) > 0
@staticmethod @staticmethod
def is_pair_locked(pair: str, now: Optional[datetime] = None, side: str = "*") -> bool: def is_pair_locked(pair: str, now: datetime | None = None, side: str = "*") -> bool:
""" """
:param pair: Pair to check for :param pair: Pair to check for
:param now: Datetime object (generated via datetime.now(timezone.utc)). :param now: Datetime object (generated via datetime.now(timezone.utc)).

View File

@@ -43,6 +43,7 @@ from freqtrade.exchange import (
amount_to_contract_precision, amount_to_contract_precision,
price_to_precision, price_to_precision,
) )
from freqtrade.exchange.exchange_types import CcxtOrder
from freqtrade.leverage import interest from freqtrade.leverage import interest
from freqtrade.misc import safe_value_fallback from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.base import ModelBase, SessionType from freqtrade.persistence.base import ModelBase, SessionType
@@ -96,26 +97,24 @@ class Order(ModelBase):
ft_cancel_reason: Mapped[str] = mapped_column(String(CUSTOM_TAG_MAX_LENGTH), nullable=True) ft_cancel_reason: Mapped[str] = mapped_column(String(CUSTOM_TAG_MAX_LENGTH), nullable=True)
order_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True) order_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
status: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) status: Mapped[str | None] = mapped_column(String(255), nullable=True)
symbol: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) symbol: Mapped[str | None] = mapped_column(String(25), nullable=True)
order_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) order_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
side: Mapped[str] = mapped_column(String(25), nullable=True) side: Mapped[str] = mapped_column(String(25), nullable=True)
price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) price: Mapped[float | None] = mapped_column(Float(), nullable=True)
average: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) average: Mapped[float | None] = mapped_column(Float(), nullable=True)
amount: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) amount: Mapped[float | None] = mapped_column(Float(), nullable=True)
filled: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) filled: Mapped[float | None] = mapped_column(Float(), nullable=True)
remaining: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) remaining: Mapped[float | None] = mapped_column(Float(), nullable=True)
cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) cost: Mapped[float | None] = mapped_column(Float(), nullable=True)
stop_price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) stop_price: Mapped[float | None] = mapped_column(Float(), nullable=True)
order_date: Mapped[datetime] = mapped_column(nullable=True, default=dt_now) order_date: Mapped[datetime] = mapped_column(nullable=True, default=dt_now)
order_filled_date: Mapped[Optional[datetime]] = mapped_column(nullable=True) order_filled_date: Mapped[datetime | None] = mapped_column(nullable=True)
order_update_date: Mapped[Optional[datetime]] = mapped_column(nullable=True) order_update_date: Mapped[datetime | None] = mapped_column(nullable=True)
funding_fee: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) funding_fee: Mapped[float | None] = mapped_column(Float(), nullable=True)
ft_fee_base: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) ft_fee_base: Mapped[float | None] = mapped_column(Float(), nullable=True)
ft_order_tag: Mapped[Optional[str]] = mapped_column( ft_order_tag: Mapped[str | None] = mapped_column(String(CUSTOM_TAG_MAX_LENGTH), nullable=True)
String(CUSTOM_TAG_MAX_LENGTH), nullable=True
)
@property @property
def order_date_utc(self) -> datetime: def order_date_utc(self) -> datetime:
@@ -123,7 +122,7 @@ class Order(ModelBase):
return self.order_date.replace(tzinfo=timezone.utc) return self.order_date.replace(tzinfo=timezone.utc)
@property @property
def order_filled_utc(self) -> Optional[datetime]: def order_filled_utc(self) -> datetime | None:
"""last order-date with UTC timezoneinfo""" """last order-date with UTC timezoneinfo"""
return ( return (
self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None
@@ -175,6 +174,11 @@ class Order(ModelBase):
"""Amount in stake currency used for this order""" """Amount in stake currency used for this order"""
return self.safe_amount * self.safe_price / self.trade.leverage return self.safe_amount * self.safe_price / self.trade.leverage
@property
def stake_amount_filled(self) -> float:
"""Filled Amount in stake currency used for this order"""
return self.safe_filled * self.safe_price / self.trade.leverage
def __repr__(self): def __repr__(self):
return ( return (
f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, " f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
@@ -309,7 +313,7 @@ class Order(ModelBase):
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct) trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct)
@staticmethod @staticmethod
def update_orders(orders: list["Order"], order: dict[str, Any]): def update_orders(orders: list["Order"], order: CcxtOrder):
""" """
Get all non-closed orders - useful when trying to batch-update orders Get all non-closed orders - useful when trying to batch-update orders
""" """
@@ -328,11 +332,11 @@ class Order(ModelBase):
@classmethod @classmethod
def parse_from_ccxt_object( def parse_from_ccxt_object(
cls, cls,
order: dict[str, Any], order: CcxtOrder,
pair: str, pair: str,
side: str, side: str,
amount: Optional[float] = None, amount: float | None = None,
price: Optional[float] = None, price: float | None = None,
) -> Self: ) -> Self:
""" """
Parse an order from a ccxt object and return a new order Object. Parse an order from a ccxt object and return a new order Object.
@@ -379,6 +383,7 @@ class LocalTrade:
# Copy of trades_open - but indexed by pair # Copy of trades_open - but indexed by pair
bt_trades_open_pp: dict[str, list["LocalTrade"]] = defaultdict(list) bt_trades_open_pp: dict[str, list["LocalTrade"]] = defaultdict(list)
bt_open_open_trade_count: int = 0 bt_open_open_trade_count: int = 0
bt_open_open_trade_count_candle: int = 0
bt_total_profit: float = 0 bt_total_profit: float = 0
realized_profit: float = 0 realized_profit: float = 0
@@ -388,57 +393,57 @@ class LocalTrade:
exchange: str = "" exchange: str = ""
pair: str = "" pair: str = ""
base_currency: Optional[str] = "" base_currency: str | None = ""
stake_currency: Optional[str] = "" stake_currency: str | None = ""
is_open: bool = True is_open: bool = True
fee_open: float = 0.0 fee_open: float = 0.0
fee_open_cost: Optional[float] = None fee_open_cost: float | None = None
fee_open_currency: Optional[str] = "" fee_open_currency: str | None = ""
fee_close: Optional[float] = 0.0 fee_close: float | None = 0.0
fee_close_cost: Optional[float] = None fee_close_cost: float | None = None
fee_close_currency: Optional[str] = "" fee_close_currency: str | None = ""
open_rate: float = 0.0 open_rate: float = 0.0
open_rate_requested: Optional[float] = None open_rate_requested: float | None = None
# open_trade_value - calculated via _calc_open_trade_value # open_trade_value - calculated via _calc_open_trade_value
open_trade_value: float = 0.0 open_trade_value: float = 0.0
close_rate: Optional[float] = None close_rate: float | None = None
close_rate_requested: Optional[float] = None close_rate_requested: float | None = None
close_profit: Optional[float] = None close_profit: float | None = None
close_profit_abs: Optional[float] = None close_profit_abs: float | None = None
stake_amount: float = 0.0 stake_amount: float = 0.0
max_stake_amount: Optional[float] = 0.0 max_stake_amount: float | None = 0.0
amount: float = 0.0 amount: float = 0.0
amount_requested: Optional[float] = None amount_requested: float | None = None
open_date: datetime open_date: datetime
close_date: Optional[datetime] = None close_date: datetime | None = None
# absolute value of the stop loss # absolute value of the stop loss
stop_loss: float = 0.0 stop_loss: float = 0.0
# percentage value of the stop loss # percentage value of the stop loss
stop_loss_pct: Optional[float] = 0.0 stop_loss_pct: float | None = 0.0
# absolute value of the initial stop loss # absolute value of the initial stop loss
initial_stop_loss: Optional[float] = 0.0 initial_stop_loss: float | None = 0.0
# percentage value of the initial stop loss # percentage value of the initial stop loss
initial_stop_loss_pct: Optional[float] = None initial_stop_loss_pct: float | None = None
is_stop_loss_trailing: bool = False is_stop_loss_trailing: bool = False
# absolute value of the highest reached price # absolute value of the highest reached price
max_rate: Optional[float] = None max_rate: float | None = None
# Lowest price reached # Lowest price reached
min_rate: Optional[float] = None min_rate: float | None = None
exit_reason: Optional[str] = "" exit_reason: str | None = ""
exit_order_status: Optional[str] = "" exit_order_status: str | None = ""
strategy: Optional[str] = "" strategy: str | None = ""
enter_tag: Optional[str] = None enter_tag: str | None = None
timeframe: Optional[int] = None timeframe: int | None = None
trading_mode: TradingMode = TradingMode.SPOT trading_mode: TradingMode = TradingMode.SPOT
amount_precision: Optional[float] = None amount_precision: float | None = None
price_precision: Optional[float] = None price_precision: float | None = None
precision_mode: Optional[int] = None precision_mode: int | None = None
precision_mode_price: Optional[int] = None precision_mode_price: int | None = None
contract_size: Optional[float] = None contract_size: float | None = None
# Leverage trading properties # Leverage trading properties
liquidation_price: Optional[float] = None liquidation_price: float | None = None
is_short: bool = False is_short: bool = False
leverage: float = 1.0 leverage: float = 1.0
@@ -446,10 +451,10 @@ class LocalTrade:
interest_rate: float = 0.0 interest_rate: float = 0.0
# Futures properties # Futures properties
funding_fees: Optional[float] = None funding_fees: float | None = None
# Used to keep running funding fees - between the last filled order and now # Used to keep running funding fees - between the last filled order and now
# Shall not be used for calculations! # Shall not be used for calculations!
funding_fee_running: Optional[float] = None funding_fee_running: float | None = None
@property @property
def stoploss_or_liquidation(self) -> float: def stoploss_or_liquidation(self) -> float:
@@ -462,7 +467,7 @@ class LocalTrade:
return self.stop_loss return self.stop_loss
@property @property
def buy_tag(self) -> Optional[str]: def buy_tag(self) -> str | None:
""" """
Compatibility between buy_tag (old) and enter_tag (new) Compatibility between buy_tag (old) and enter_tag (new)
Consider buy_tag deprecated Consider buy_tag deprecated
@@ -489,7 +494,7 @@ class LocalTrade:
return self.amount return self.amount
@property @property
def _date_last_filled_utc(self) -> Optional[datetime]: def _date_last_filled_utc(self) -> datetime | None:
"""Date of the last filled order""" """Date of the last filled order"""
orders = self.select_filled_orders() orders = self.select_filled_orders()
if orders: if orders:
@@ -505,7 +510,7 @@ class LocalTrade:
return max([self.open_date_utc, dt_last_filled]) return max([self.open_date_utc, dt_last_filled])
@property @property
def date_entry_fill_utc(self) -> Optional[datetime]: def date_entry_fill_utc(self) -> datetime | None:
"""Date of the first filled order""" """Date of the first filled order"""
orders = self.select_filled_orders(self.entry_side) orders = self.select_filled_orders(self.entry_side)
if orders and len( if orders and len(
@@ -747,6 +752,7 @@ class LocalTrade:
LocalTrade.bt_trades_open = [] LocalTrade.bt_trades_open = []
LocalTrade.bt_trades_open_pp = defaultdict(list) LocalTrade.bt_trades_open_pp = defaultdict(list)
LocalTrade.bt_open_open_trade_count = 0 LocalTrade.bt_open_open_trade_count = 0
LocalTrade.bt_open_open_trade_count_candle = 0
LocalTrade.bt_total_profit = 0 LocalTrade.bt_total_profit = 0
def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None: def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None:
@@ -756,7 +762,7 @@ class LocalTrade:
self.max_rate = max(current_price, self.max_rate or self.open_rate) self.max_rate = max(current_price, self.max_rate or self.open_rate)
self.min_rate = min(current_price_low, self.min_rate or self.open_rate) self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
def set_liquidation_price(self, liquidation_price: Optional[float]): def set_liquidation_price(self, liquidation_price: float | None):
""" """
Method you should use to set self.liquidation price. Method you should use to set self.liquidation price.
Assures stop_loss is not passed the liquidation price Assures stop_loss is not passed the liquidation price
@@ -788,7 +794,7 @@ class LocalTrade:
def adjust_stop_loss( def adjust_stop_loss(
self, self,
current_price: float, current_price: float,
stoploss: Optional[float], stoploss: float | None,
initial: bool = False, initial: bool = False,
allow_refresh: bool = False, allow_refresh: bool = False,
) -> None: ) -> None:
@@ -928,7 +934,7 @@ class LocalTrade:
) )
def update_fee( def update_fee(
self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float], side: str self, fee_cost: float, fee_currency: str | None, fee_rate: float | None, side: str
) -> None: ) -> None:
""" """
Update Fee parameters. Only acts once per side Update Fee parameters. Only acts once per side
@@ -957,7 +963,7 @@ class LocalTrade:
else: else:
return False return False
def update_order(self, order: dict) -> None: def update_order(self, order: CcxtOrder) -> None:
Order.update_orders(self.orders, order) Order.update_orders(self.orders, order)
@property @property
@@ -1036,7 +1042,7 @@ class LocalTrade:
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours) return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: FtPrecise, rate: float, fee: Optional[float]) -> FtPrecise: def _calc_base_close(self, amount: FtPrecise, rate: float, fee: float | None) -> FtPrecise:
close_trade = amount * FtPrecise(rate) close_trade = amount * FtPrecise(rate)
fees = close_trade * FtPrecise(fee or 0.0) fees = close_trade * FtPrecise(fee or 0.0)
@@ -1045,7 +1051,7 @@ class LocalTrade:
else: else:
return close_trade - fees return close_trade - fees
def calc_close_trade_value(self, rate: float, amount: Optional[float] = None) -> float: def calc_close_trade_value(self, rate: float, amount: float | None = None) -> float:
""" """
Calculate the Trade's close value including fees Calculate the Trade's close value including fees
:param rate: rate to compare with. :param rate: rate to compare with.
@@ -1084,7 +1090,7 @@ class LocalTrade:
) )
def calc_profit( def calc_profit(
self, rate: float, amount: Optional[float] = None, open_rate: Optional[float] = None self, rate: float, amount: float | None = None, open_rate: float | None = None
) -> float: ) -> float:
""" """
Calculate the absolute profit in stake currency between Close and Open trade Calculate the absolute profit in stake currency between Close and Open trade
@@ -1098,7 +1104,7 @@ class LocalTrade:
return prof.profit_abs return prof.profit_abs
def calculate_profit( def calculate_profit(
self, rate: float, amount: Optional[float] = None, open_rate: Optional[float] = None self, rate: float, amount: float | None = None, open_rate: float | None = None
) -> ProfitStruct: ) -> ProfitStruct:
""" """
Calculate profit metrics (absolute, ratio, total, total ratio). Calculate profit metrics (absolute, ratio, total, total ratio).
@@ -1146,7 +1152,7 @@ class LocalTrade:
) )
def calc_profit_ratio( def calc_profit_ratio(
self, rate: float, amount: Optional[float] = None, open_rate: Optional[float] = None self, rate: float, amount: float | None = None, open_rate: float | None = None
) -> float: ) -> float:
""" """
Calculates the profit as ratio (including fee). Calculates the profit as ratio (including fee).
@@ -1246,7 +1252,7 @@ class LocalTrade:
self.close_profit = (close_profit_abs / total_stake) * self.leverage self.close_profit = (close_profit_abs / total_stake) * self.leverage
self.close_profit_abs = close_profit_abs self.close_profit_abs = close_profit_abs
def select_order_by_order_id(self, order_id: str) -> Optional[Order]: def select_order_by_order_id(self, order_id: str) -> Order | None:
""" """
Finds order object by Order id. Finds order object by Order id.
:param order_id: Exchange order id :param order_id: Exchange order id
@@ -1258,10 +1264,10 @@ class LocalTrade:
def select_order( def select_order(
self, self,
order_side: Optional[str] = None, order_side: str | None = None,
is_open: Optional[bool] = None, is_open: bool | None = None,
only_filled: bool = False, only_filled: bool = False,
) -> Optional[Order]: ) -> Order | None:
""" """
Finds latest order for this orderside and status Finds latest order for this orderside and status
:param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss') :param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
@@ -1281,7 +1287,7 @@ class LocalTrade:
else: else:
return None return None
def select_filled_orders(self, order_side: Optional[str] = None) -> list["Order"]: def select_filled_orders(self, order_side: str | None = None) -> list["Order"]:
""" """
Finds filled orders for this order side. Finds filled orders for this order side.
Will not return open orders which already partially filled. Will not return open orders which already partially filled.
@@ -1332,7 +1338,7 @@ class LocalTrade:
return data[0].value return data[0].value
return default return default
def get_custom_data_entry(self, key: str) -> Optional[_CustomData]: def get_custom_data_entry(self, key: str) -> _CustomData | None:
""" """
Get custom data for this trade Get custom data for this trade
:param key: key of the custom data :param key: key of the custom data
@@ -1385,7 +1391,7 @@ class LocalTrade:
return len(self.select_filled_orders("sell")) return len(self.select_filled_orders("sell"))
@property @property
def sell_reason(self) -> Optional[str]: def sell_reason(self) -> str | None:
"""DEPRECATED! Please use exit_reason instead.""" """DEPRECATED! Please use exit_reason instead."""
return self.exit_reason return self.exit_reason
@@ -1396,10 +1402,10 @@ class LocalTrade:
@staticmethod @staticmethod
def get_trades_proxy( def get_trades_proxy(
*, *,
pair: Optional[str] = None, pair: str | None = None,
is_open: Optional[bool] = None, is_open: bool | None = None,
open_date: Optional[datetime] = None, open_date: datetime | None = None,
close_date: Optional[datetime] = None, close_date: datetime | None = None,
) -> list["LocalTrade"]: ) -> list["LocalTrade"]:
""" """
Helper function to query Trades. Helper function to query Trades.
@@ -1442,6 +1448,11 @@ class LocalTrade:
LocalTrade.bt_trades_open.remove(trade) LocalTrade.bt_trades_open.remove(trade)
LocalTrade.bt_trades_open_pp[trade.pair].remove(trade) LocalTrade.bt_trades_open_pp[trade.pair].remove(trade)
LocalTrade.bt_open_open_trade_count -= 1 LocalTrade.bt_open_open_trade_count -= 1
if (trade.close_date_utc - trade.open_date_utc) > timedelta(minutes=trade.timeframe):
# Only subtract trades that are open for more than 1 candle
# To avoid exceeding max_open_trades.
# Must be reset at the start of every candle during backesting.
LocalTrade.bt_open_open_trade_count_candle -= 1
LocalTrade.bt_trades.append(trade) LocalTrade.bt_trades.append(trade)
LocalTrade.bt_total_profit += trade.close_profit_abs LocalTrade.bt_total_profit += trade.close_profit_abs
@@ -1451,6 +1462,7 @@ class LocalTrade:
LocalTrade.bt_trades_open.append(trade) LocalTrade.bt_trades_open.append(trade)
LocalTrade.bt_trades_open_pp[trade.pair].append(trade) LocalTrade.bt_trades_open_pp[trade.pair].append(trade)
LocalTrade.bt_open_open_trade_count += 1 LocalTrade.bt_open_open_trade_count += 1
LocalTrade.bt_open_open_trade_count_candle += 1
else: else:
LocalTrade.bt_trades.append(trade) LocalTrade.bt_trades.append(trade)
@@ -1459,6 +1471,9 @@ class LocalTrade:
LocalTrade.bt_trades_open.remove(trade) LocalTrade.bt_trades_open.remove(trade)
LocalTrade.bt_trades_open_pp[trade.pair].remove(trade) LocalTrade.bt_trades_open_pp[trade.pair].remove(trade)
LocalTrade.bt_open_open_trade_count -= 1 LocalTrade.bt_open_open_trade_count -= 1
# TODO: The below may have odd behavior in case of canceled entries
# It might need to be removed so the trade "counts" as open for this candle.
LocalTrade.bt_open_open_trade_count_candle -= 1
@staticmethod @staticmethod
def get_open_trades() -> list[Any]: def get_open_trades() -> list[Any]:
@@ -1619,92 +1634,92 @@ class Trade(ModelBase, LocalTrade):
exchange: Mapped[str] = mapped_column(String(25), nullable=False) # type: ignore exchange: Mapped[str] = mapped_column(String(25), nullable=False) # type: ignore
pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # type: ignore pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # type: ignore
base_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # type: ignore base_currency: Mapped[str | None] = mapped_column(String(25), nullable=True) # type: ignore
stake_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # type: ignore stake_currency: Mapped[str | None] = mapped_column(String(25), nullable=True) # type: ignore
is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) # type: ignore is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) # type: ignore
fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) # type: ignore fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) # type: ignore
fee_open_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore fee_open_cost: Mapped[float | None] = mapped_column(Float(), nullable=True) # type: ignore
fee_open_currency: Mapped[Optional[str]] = mapped_column( # type: ignore fee_open_currency: Mapped[str | None] = mapped_column( # type: ignore
String(25), nullable=True String(25), nullable=True
) )
fee_close: Mapped[Optional[float]] = mapped_column( # type: ignore fee_close: Mapped[float | None] = mapped_column( # type: ignore
Float(), nullable=False, default=0.0 Float(), nullable=False, default=0.0
) )
fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore fee_close_cost: Mapped[float | None] = mapped_column(Float(), nullable=True) # type: ignore
fee_close_currency: Mapped[Optional[str]] = mapped_column( # type: ignore fee_close_currency: Mapped[str | None] = mapped_column( # type: ignore
String(25), nullable=True String(25), nullable=True
) )
open_rate: Mapped[float] = mapped_column(Float()) # type: ignore open_rate: Mapped[float] = mapped_column(Float()) # type: ignore
open_rate_requested: Mapped[Optional[float]] = mapped_column( # type: ignore open_rate_requested: Mapped[float | None] = mapped_column( # type: ignore
Float(), nullable=True Float(), nullable=True
) )
# open_trade_value - calculated via _calc_open_trade_value # open_trade_value - calculated via _calc_open_trade_value
open_trade_value: Mapped[float] = mapped_column(Float(), nullable=True) # type: ignore open_trade_value: Mapped[float] = mapped_column(Float(), nullable=True) # type: ignore
close_rate: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore close_rate: Mapped[float | None] = mapped_column(Float()) # type: ignore
close_rate_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore close_rate_requested: Mapped[float | None] = mapped_column(Float()) # type: ignore
realized_profit: Mapped[float] = mapped_column( # type: ignore realized_profit: Mapped[float] = mapped_column( # type: ignore
Float(), default=0.0, nullable=True Float(), default=0.0, nullable=True
) )
close_profit: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore close_profit: Mapped[float | None] = mapped_column(Float()) # type: ignore
close_profit_abs: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore close_profit_abs: Mapped[float | None] = mapped_column(Float()) # type: ignore
stake_amount: Mapped[float] = mapped_column(Float(), nullable=False) # type: ignore stake_amount: Mapped[float] = mapped_column(Float(), nullable=False) # type: ignore
max_stake_amount: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore max_stake_amount: Mapped[float | None] = mapped_column(Float()) # type: ignore
amount: Mapped[float] = mapped_column(Float()) # type: ignore amount: Mapped[float] = mapped_column(Float()) # type: ignore
amount_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore amount_requested: Mapped[float | None] = mapped_column(Float()) # type: ignore
open_date: Mapped[datetime] = mapped_column( # type: ignore open_date: Mapped[datetime] = mapped_column( # type: ignore
nullable=False, default=datetime.now nullable=False, default=datetime.now
) )
close_date: Mapped[Optional[datetime]] = mapped_column() # type: ignore close_date: Mapped[datetime | None] = mapped_column() # type: ignore
# absolute value of the stop loss # absolute value of the stop loss
stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore
# percentage value of the stop loss # percentage value of the stop loss
stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore stop_loss_pct: Mapped[float | None] = mapped_column(Float(), nullable=True) # type: ignore
# absolute value of the initial stop loss # absolute value of the initial stop loss
initial_stop_loss: Mapped[Optional[float]] = mapped_column( # type: ignore initial_stop_loss: Mapped[float | None] = mapped_column( # type: ignore
Float(), nullable=True, default=0.0 Float(), nullable=True, default=0.0
) )
# percentage value of the initial stop loss # percentage value of the initial stop loss
initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column( # type: ignore initial_stop_loss_pct: Mapped[float | None] = mapped_column( # type: ignore
Float(), nullable=True Float(), nullable=True
) )
is_stop_loss_trailing: Mapped[bool] = mapped_column( # type: ignore is_stop_loss_trailing: Mapped[bool] = mapped_column( # type: ignore
nullable=False, default=False nullable=False, default=False
) )
# absolute value of the highest reached price # absolute value of the highest reached price
max_rate: Mapped[Optional[float]] = mapped_column( # type: ignore max_rate: Mapped[float | None] = mapped_column( # type: ignore
Float(), nullable=True, default=0.0 Float(), nullable=True, default=0.0
) )
# Lowest price reached # Lowest price reached
min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore min_rate: Mapped[float | None] = mapped_column(Float(), nullable=True) # type: ignore
exit_reason: Mapped[Optional[str]] = mapped_column( # type: ignore exit_reason: Mapped[str | None] = mapped_column( # type: ignore
String(CUSTOM_TAG_MAX_LENGTH), nullable=True String(CUSTOM_TAG_MAX_LENGTH), nullable=True
) )
exit_order_status: Mapped[Optional[str]] = mapped_column( # type: ignore exit_order_status: Mapped[str | None] = mapped_column( # type: ignore
String(100), nullable=True String(100), nullable=True
) )
strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore strategy: Mapped[str | None] = mapped_column(String(100), nullable=True) # type: ignore
enter_tag: Mapped[Optional[str]] = mapped_column( # type: ignore enter_tag: Mapped[str | None] = mapped_column( # type: ignore
String(CUSTOM_TAG_MAX_LENGTH), nullable=True String(CUSTOM_TAG_MAX_LENGTH), nullable=True
) )
timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore timeframe: Mapped[int | None] = mapped_column(Integer, nullable=True) # type: ignore
trading_mode: Mapped[TradingMode] = mapped_column( # type: ignore trading_mode: Mapped[TradingMode] = mapped_column( # type: ignore
Enum(TradingMode), nullable=True Enum(TradingMode), nullable=True
) )
amount_precision: Mapped[Optional[float]] = mapped_column( # type: ignore amount_precision: Mapped[float | None] = mapped_column( # type: ignore
Float(), nullable=True Float(), nullable=True
) )
price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore price_precision: Mapped[float | None] = mapped_column(Float(), nullable=True) # type: ignore
precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore precision_mode: Mapped[int | None] = mapped_column(Integer, nullable=True) # type: ignore
precision_mode_price: Mapped[Optional[int]] = mapped_column( # type: ignore precision_mode_price: Mapped[int | None] = mapped_column( # type: ignore
Integer, nullable=True Integer, nullable=True
) )
contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore contract_size: Mapped[float | None] = mapped_column(Float(), nullable=True) # type: ignore
# Leverage trading properties # Leverage trading properties
leverage: Mapped[float] = mapped_column(Float(), nullable=True, default=1.0) # type: ignore leverage: Mapped[float] = mapped_column(Float(), nullable=True, default=1.0) # type: ignore
is_short: Mapped[bool] = mapped_column(nullable=False, default=False) # type: ignore is_short: Mapped[bool] = mapped_column(nullable=False, default=False) # type: ignore
liquidation_price: Mapped[Optional[float]] = mapped_column( # type: ignore liquidation_price: Mapped[float | None] = mapped_column( # type: ignore
Float(), nullable=True Float(), nullable=True
) )
@@ -1714,10 +1729,10 @@ class Trade(ModelBase, LocalTrade):
) )
# Futures properties # Futures properties
funding_fees: Mapped[Optional[float]] = mapped_column( # type: ignore funding_fees: Mapped[float | None] = mapped_column( # type: ignore
Float(), nullable=True, default=None Float(), nullable=True, default=None
) )
funding_fee_running: Mapped[Optional[float]] = mapped_column( # type: ignore funding_fee_running: Mapped[float | None] = mapped_column( # type: ignore
Float(), nullable=True, default=None Float(), nullable=True, default=None
) )
@@ -1756,10 +1771,10 @@ class Trade(ModelBase, LocalTrade):
@staticmethod @staticmethod
def get_trades_proxy( def get_trades_proxy(
*, *,
pair: Optional[str] = None, pair: str | None = None,
is_open: Optional[bool] = None, is_open: bool | None = None,
open_date: Optional[datetime] = None, open_date: datetime | None = None,
close_date: Optional[datetime] = None, close_date: datetime | None = None,
) -> list["LocalTrade"]: ) -> list["LocalTrade"]:
""" """
Helper function to query Trades.j Helper function to query Trades.j
@@ -1922,7 +1937,7 @@ class Trade(ModelBase, LocalTrade):
] ]
@staticmethod @staticmethod
def get_enter_tag_performance(pair: Optional[str]) -> list[dict[str, Any]]: def get_enter_tag_performance(pair: str | None) -> list[dict[str, Any]]:
""" """
Returns List of dicts containing all Trades, based on buy tag performance Returns List of dicts containing all Trades, based on buy tag performance
Can either be average for all pairs or a specific pair provided Can either be average for all pairs or a specific pair provided
@@ -1957,7 +1972,7 @@ class Trade(ModelBase, LocalTrade):
] ]
@staticmethod @staticmethod
def get_exit_reason_performance(pair: Optional[str]) -> list[dict[str, Any]]: def get_exit_reason_performance(pair: str | None) -> list[dict[str, Any]]:
""" """
Returns List of dicts containing all Trades, based on exit reason performance Returns List of dicts containing all Trades, based on exit reason performance
Can either be average for all pairs or a specific pair provided Can either be average for all pairs or a specific pair provided
@@ -1991,7 +2006,7 @@ class Trade(ModelBase, LocalTrade):
] ]
@staticmethod @staticmethod
def get_mix_tag_performance(pair: Optional[str]) -> list[dict[str, Any]]: def get_mix_tag_performance(pair: str | None) -> list[dict[str, Any]]:
""" """
Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
Can either be average for all pairs or a specific pair provided Can either be average for all pairs or a specific pair provided
@@ -2048,7 +2063,7 @@ class Trade(ModelBase, LocalTrade):
return resp return resp
@staticmethod @staticmethod
def get_best_pair(start_date: Optional[datetime] = None): def get_best_pair(start_date: datetime | None = None):
""" """
Get best pair with closed trade. Get best pair with closed trade.
NOTE: Not supported in Backtesting. NOTE: Not supported in Backtesting.
@@ -2068,7 +2083,7 @@ class Trade(ModelBase, LocalTrade):
return best_pair return best_pair
@staticmethod @staticmethod
def get_trading_volume(start_date: Optional[datetime] = None) -> float: def get_trading_volume(start_date: datetime | None = None) -> float:
""" """
Get Trade volume based on Orders Get Trade volume based on Orders
NOTE: Not supported in Backtesting. NOTE: Not supported in Backtesting.

View File

@@ -1,7 +1,6 @@
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Optional
import pandas as pd import pandas as pd
@@ -406,7 +405,7 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots:
return fig return fig
def create_scatter(data, column_name, color, direction) -> Optional[go.Scatter]: def create_scatter(data, column_name, color, direction) -> go.Scatter | None:
if column_name in data.columns: if column_name in data.columns:
df_short = data[data[column_name] == 1] df_short = data[data[column_name] == 1]
if len(df_short) > 0: if len(df_short) > 0:
@@ -432,11 +431,11 @@ def create_scatter(data, column_name, color, direction) -> Optional[go.Scatter]:
def generate_candlestick_graph( def generate_candlestick_graph(
pair: str, pair: str,
data: pd.DataFrame, data: pd.DataFrame,
trades: Optional[pd.DataFrame] = None, trades: pd.DataFrame | None = None,
*, *,
indicators1: Optional[list[str]] = None, indicators1: list[str] | None = None,
indicators2: Optional[list[str]] = None, indicators2: list[str] | None = None,
plot_config: Optional[dict[str, dict]] = None, plot_config: dict[str, dict] | None = None,
) -> go.Figure: ) -> go.Figure:
""" """
Generate the graph from the data generated by Backtesting or from DB Generate the graph from the data generated by Backtesting or from DB

View File

@@ -5,7 +5,6 @@ Minimum age (days listed) pair list filter
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import timedelta from datetime import timedelta
from typing import Optional
from pandas import DataFrame from pandas import DataFrame
@@ -126,7 +125,7 @@ class AgeFilter(IPairList):
self.log_once(f"Validated {len(pairlist)} pairs.", logger.info) self.log_once(f"Validated {len(pairlist)} pairs.", logger.info)
return pairlist return pairlist
def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: def _validate_pair_loc(self, pair: str, daily_candles: DataFrame | None) -> bool:
""" """
Validate age for the ticker Validate age for the ticker
:param pair: Pair that's currently validated :param pair: Pair that's currently validated

View File

@@ -6,7 +6,7 @@ import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from copy import deepcopy from copy import deepcopy
from enum import Enum from enum import Enum
from typing import Any, Literal, Optional, TypedDict, Union from typing import Any, Literal, TypedDict
from freqtrade.constants import Config from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@@ -25,37 +25,37 @@ class __PairlistParameterBase(TypedDict):
class __NumberPairlistParameter(__PairlistParameterBase): class __NumberPairlistParameter(__PairlistParameterBase):
type: Literal["number"] type: Literal["number"]
default: Union[int, float, None] default: int | float | None
class __StringPairlistParameter(__PairlistParameterBase): class __StringPairlistParameter(__PairlistParameterBase):
type: Literal["string"] type: Literal["string"]
default: Union[str, None] default: str | None
class __OptionPairlistParameter(__PairlistParameterBase): class __OptionPairlistParameter(__PairlistParameterBase):
type: Literal["option"] type: Literal["option"]
default: Union[str, None] default: str | None
options: list[str] options: list[str]
class __ListPairListParamenter(__PairlistParameterBase): class __ListPairListParamenter(__PairlistParameterBase):
type: Literal["list"] type: Literal["list"]
default: Union[list[str], None] default: list[str] | None
class __BoolPairlistParameter(__PairlistParameterBase): class __BoolPairlistParameter(__PairlistParameterBase):
type: Literal["boolean"] type: Literal["boolean"]
default: Union[bool, None] default: bool | None
PairlistParameter = Union[ PairlistParameter = (
__NumberPairlistParameter, __NumberPairlistParameter
__StringPairlistParameter, | __StringPairlistParameter
__OptionPairlistParameter, | __OptionPairlistParameter
__BoolPairlistParameter, | __BoolPairlistParameter
__ListPairListParamenter, | __ListPairListParamenter
] )
class SupportsBacktesting(str, Enum): class SupportsBacktesting(str, Enum):
@@ -153,7 +153,7 @@ class IPairList(LoggingMixin, ABC):
-> Please overwrite in subclasses -> Please overwrite in subclasses
""" """
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool: def _validate_pair(self, pair: str, ticker: Ticker | None) -> bool:
""" """
Check one pair against Pairlist Handler's specific conditions. Check one pair against Pairlist Handler's specific conditions.

View File

@@ -5,6 +5,7 @@ Provides dynamic pair list based on Market Cap
""" """
import logging import logging
import math
from cachetools import TTLCache from cachetools import TTLCache
@@ -57,7 +58,11 @@ class MarketCapPairList(IPairList):
) )
if self._max_rank > 250: if self._max_rank > 250:
raise OperationalException("This filter only support marketcap rank up to 250.") self.logger.warning(
f"The max rank you have set ({self._max_rank}) is quite high. "
"This may lead to coingecko API rate limit issues. "
"Please ensure this value is necessary for your use case.",
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@@ -165,7 +170,11 @@ class MarketCapPairList(IPairList):
data = [] data = []
if not self._categories: if not self._categories:
data = self._coingecko.get_coins_markets(**default_kwargs) pages_required = math.ceil(self._max_rank / 250)
for page in range(1, pages_required + 1):
default_kwargs["page"] = str(page)
page_data = self._coingecko.get_coins_markets(**default_kwargs)
data.extend(page_data)
else: else:
for category in self._categories: for category in self._categories:
category_data = self._coingecko.get_coins_markets( category_data = self._coingecko.get_coins_markets(

View File

@@ -8,7 +8,7 @@ defined period or as coming from ticker
import logging import logging
from datetime import timedelta from datetime import timedelta
from typing import Any, Optional from typing import Any
from cachetools import TTLCache from cachetools import TTLCache
from pandas import DataFrame from pandas import DataFrame
@@ -46,7 +46,7 @@ class PercentChangePairList(IPairList):
self._lookback_days = self._pairlistconfig.get("lookback_days", 0) self._lookback_days = self._pairlistconfig.get("lookback_days", 0)
self._lookback_timeframe = self._pairlistconfig.get("lookback_timeframe", "1d") self._lookback_timeframe = self._pairlistconfig.get("lookback_timeframe", "1d")
self._lookback_period = self._pairlistconfig.get("lookback_period", 0) self._lookback_period = self._pairlistconfig.get("lookback_period", 0)
self._sort_direction: Optional[str] = self._pairlistconfig.get("sort_direction", "desc") self._sort_direction: str | None = self._pairlistconfig.get("sort_direction", "desc")
self._def_candletype = self._config["candle_type_def"] self._def_candletype = self._config["candle_type_def"]
if (self._lookback_days > 0) & (self._lookback_period > 0): if (self._lookback_days > 0) & (self._lookback_period > 0):
@@ -311,7 +311,7 @@ class PercentChangePairList(IPairList):
else: else:
filtered_tickers[i]["percentage"] = tickers[p["symbol"]]["percentage"] filtered_tickers[i]["percentage"] = tickers[p["symbol"]]["percentage"]
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool: def _validate_pair(self, pair: str, ticker: Ticker | None) -> bool:
""" """
Check if one price-step (pip) is > than a certain barrier. Check if one price-step (pip) is > than a certain barrier.
:param pair: Pair that's currently validated :param pair: Pair that's currently validated

View File

@@ -3,7 +3,6 @@ Precision pair list filter
""" """
import logging import logging
from typing import Optional
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import ROUND_UP from freqtrade.exchange import ROUND_UP
@@ -50,7 +49,7 @@ class PrecisionFilter(IPairList):
def description() -> str: def description() -> str:
return "Filters low-value coins which would not allow setting stoplosses." return "Filters low-value coins which would not allow setting stoplosses."
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool: def _validate_pair(self, pair: str, ticker: Ticker | None) -> bool:
""" """
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
low value pairs. low value pairs.

View File

@@ -3,7 +3,6 @@ Price pair list filter
""" """
import logging import logging
from typing import Optional
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.exchange_types import Ticker from freqtrade.exchange.exchange_types import Ticker
@@ -101,7 +100,7 @@ class PriceFilter(IPairList):
}, },
} }
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool: def _validate_pair(self, pair: str, ticker: Ticker | None) -> bool:
""" """
Check if one price-step (pip) is > than a certain barrier. Check if one price-step (pip) is > than a certain barrier.
:param pair: Pair that's currently validated :param pair: Pair that's currently validated

View File

@@ -5,7 +5,6 @@ Provides pair list from Leader data
""" """
import logging import logging
from typing import Optional
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.exchange_types import Tickers from freqtrade.exchange.exchange_types import Tickers
@@ -83,7 +82,7 @@ class ProducerPairList(IPairList):
}, },
} }
def _filter_pairlist(self, pairlist: Optional[list[str]]): def _filter_pairlist(self, pairlist: list[str] | None):
upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs( upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(
self._producer_name self._producer_name
) )

View File

@@ -3,7 +3,6 @@ Spread pair list filter
""" """
import logging import logging
from typing import Optional
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.exchange_types import Ticker from freqtrade.exchange.exchange_types import Ticker
@@ -61,7 +60,7 @@ class SpreadFilter(IPairList):
}, },
} }
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool: def _validate_pair(self, pair: str, ticker: Ticker | None) -> bool:
""" """
Validate spread for the ticker Validate spread for the ticker
:param pair: Pair that's currently validated :param pair: Pair that's currently validated

View File

@@ -5,7 +5,6 @@ Volatility pairlist filter
import logging import logging
import sys import sys
from datetime import timedelta from datetime import timedelta
from typing import Optional
import numpy as np import numpy as np
from cachetools import TTLCache from cachetools import TTLCache
@@ -37,7 +36,7 @@ class VolatilityFilter(IPairList):
self._max_volatility = self._pairlistconfig.get("max_volatility", sys.maxsize) self._max_volatility = self._pairlistconfig.get("max_volatility", sys.maxsize)
self._refresh_period = self._pairlistconfig.get("refresh_period", 1440) self._refresh_period = self._pairlistconfig.get("refresh_period", 1440)
self._def_candletype = self._config["candle_type_def"] self._def_candletype = self._config["candle_type_def"]
self._sort_direction: Optional[str] = self._pairlistconfig.get("sort_direction", None) self._sort_direction: str | None = self._pairlistconfig.get("sort_direction", None)
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
@@ -147,7 +146,7 @@ class VolatilityFilter(IPairList):
) )
return resulting_pairlist return resulting_pairlist
def _calculate_volatility(self, pair: str, daily_candles: DataFrame) -> Optional[float]: def _calculate_volatility(self, pair: str, daily_candles: DataFrame) -> float | None:
# Check symbol in cache # Check symbol in cache
if (volatility_avg := self._pair_cache.get(pair, None)) is not None: if (volatility_avg := self._pair_cache.get(pair, None)) is not None:
return volatility_avg return volatility_avg

View File

@@ -4,7 +4,6 @@ Rate of change pairlist filter
import logging import logging
from datetime import timedelta from datetime import timedelta
from typing import Optional
from cachetools import TTLCache from cachetools import TTLCache
from pandas import DataFrame from pandas import DataFrame
@@ -31,7 +30,7 @@ class RangeStabilityFilter(IPairList):
self._max_rate_of_change = self._pairlistconfig.get("max_rate_of_change") self._max_rate_of_change = self._pairlistconfig.get("max_rate_of_change")
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400) self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
self._def_candletype = self._config["candle_type_def"] self._def_candletype = self._config["candle_type_def"]
self._sort_direction: Optional[str] = self._pairlistconfig.get("sort_direction", None) self._sort_direction: str | None = self._pairlistconfig.get("sort_direction", None)
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
@@ -143,7 +142,7 @@ class RangeStabilityFilter(IPairList):
) )
return resulting_pairlist return resulting_pairlist
def _calculate_rate_of_change(self, pair: str, daily_candles: DataFrame) -> Optional[float]: def _calculate_rate_of_change(self, pair: str, daily_candles: DataFrame) -> float | None:
# Check symbol in cache # Check symbol in cache
if (pct_change := self._pair_cache.get(pair, None)) is not None: if (pct_change := self._pair_cache.get(pair, None)) is not None:
return pct_change return pct_change

View File

@@ -4,7 +4,6 @@ PairList manager class
import logging import logging
from functools import partial from functools import partial
from typing import Optional
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
@@ -24,16 +23,14 @@ logger = logging.getLogger(__name__)
class PairListManager(LoggingMixin): class PairListManager(LoggingMixin):
def __init__( def __init__(self, exchange, config: Config, dataprovider: DataProvider | None = None) -> None:
self, exchange, config: Config, dataprovider: Optional[DataProvider] = None
) -> None:
self._exchange = exchange self._exchange = exchange
self._config = config self._config = config
self._whitelist = self._config["exchange"].get("pair_whitelist") self._whitelist = self._config["exchange"].get("pair_whitelist")
self._blacklist = self._config["exchange"].get("pair_blacklist", []) self._blacklist = self._config["exchange"].get("pair_blacklist", [])
self._pairlist_handlers: list[IPairList] = [] self._pairlist_handlers: list[IPairList] = []
self._tickers_needed = False self._tickers_needed = False
self._dataprovider: Optional[DataProvider] = dataprovider self._dataprovider: DataProvider | None = dataprovider
for pairlist_handler_config in self._config.get("pairlists", []): for pairlist_handler_config in self._config.get("pairlists", []):
pairlist_handler = PairListResolver.load_pairlist( pairlist_handler = PairListResolver.load_pairlist(
pairlist_handler_config["method"], pairlist_handler_config["method"],
@@ -193,7 +190,7 @@ class PairListManager(LoggingMixin):
return whitelist return whitelist
def create_pair_list( def create_pair_list(
self, pairs: list[str], timeframe: Optional[str] = None self, pairs: list[str], timeframe: str | None = None
) -> ListPairsWithTimeframes: ) -> ListPairsWithTimeframes:
""" """
Create list of pair tuples with (pair, timeframe) Create list of pair tuples with (pair, timeframe)

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