Merge branch 'develop' into pr/Axel-CH/10062
This commit is contained in:
+44
-14
@@ -276,14 +276,23 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Pip cache (Windows)
|
||||
uses: actions/cache@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
path: ~\AppData\Local\pip\Cache
|
||||
key: pip-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "requirements**.txt"
|
||||
cache-suffix: "${{ matrix.python-version }}"
|
||||
prune-cache: false
|
||||
|
||||
- name: Installation
|
||||
run: |
|
||||
uv venv
|
||||
.venv\Scripts\activate
|
||||
# persist the venv path for future steps
|
||||
"$(pwd)/.venv/Scripts" >> $env:GITHUB_PATH
|
||||
|
||||
function uvpipFunction { uv pip $args }
|
||||
Set-Alias -name pip -value uvpipFunction
|
||||
./build_helpers/install_windows.ps1
|
||||
|
||||
- name: Tests
|
||||
@@ -517,13 +526,40 @@ jobs:
|
||||
ft_client/dist
|
||||
retention-days: 10
|
||||
|
||||
deploy-pypi:
|
||||
name: "Deploy to PyPI"
|
||||
deploy-test-pypi:
|
||||
name: "Publish Python 🐍 distribution 📦 to TestPyPI"
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-22.04
|
||||
if: (github.event_name == 'release')
|
||||
environment:
|
||||
name: release
|
||||
name: testpypi
|
||||
url: https://test.pypi.org/p/freqtrade
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: freqtrade*-build
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.3
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
|
||||
|
||||
deploy-pypi:
|
||||
name: "Publish Python 🐍 distribution 📦 to PyPI"
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-22.04
|
||||
if: (github.event_name == 'release')
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/freqtrade
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -538,14 +574,8 @@ jobs:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.2
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.2
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.3
|
||||
|
||||
|
||||
deploy-docker:
|
||||
|
||||
@@ -17,8 +17,8 @@ repos:
|
||||
- types-cachetools==5.5.0.20240820
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.0.20241016
|
||||
- types-tabulate==0.9.0.20240106
|
||||
- types-python-dateutil==2.9.0.20241003
|
||||
- types-tabulate==0.9.0.20241207
|
||||
- types-python-dateutil==2.9.0.20241206
|
||||
- SQLAlchemy==2.0.36
|
||||
# stages: [push]
|
||||
|
||||
@@ -31,7 +31,7 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.8.0'
|
||||
rev: 'v0.8.3'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
@@ -56,6 +56,11 @@ repos:
|
||||
.*\.md
|
||||
)$
|
||||
|
||||
- repo: https://github.com/stefmolin/exif-stripper
|
||||
rev: 0.6.1
|
||||
hooks:
|
||||
- id: strip-exif
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,11 +1,10 @@
|
||||
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
|
||||
|
||||
python -m pip install --upgrade pip wheel
|
||||
python -m pip install --upgrade pip
|
||||
python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||
|
||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||
|
||||
|
||||
pip install --find-links=build_helpers\ --prefer-binary TA-Lib
|
||||
pip install -U wheel "numpy<2"
|
||||
pip install --only-binary ta-lib --find-links=build_helpers\ ta-lib
|
||||
|
||||
pip install -r requirements-dev.txt
|
||||
pip install -e .
|
||||
|
||||
BIN
Binary file not shown.
@@ -102,8 +102,17 @@
|
||||
},
|
||||
"dry_run_wallet": {
|
||||
"description": "Initial wallet balance for dry run mode.",
|
||||
"type": "number",
|
||||
"default": 1000
|
||||
"type": [
|
||||
"number",
|
||||
"object"
|
||||
],
|
||||
"default": 1000,
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9]+$": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"cancel_open_orders_on_exit": {
|
||||
"description": "Cancel open orders when exiting.",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -39,6 +39,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
|
||||
config: Config,
|
||||
processed: dict[str, DataFrame],
|
||||
backtest_stats: dict[str, Any],
|
||||
starting_balance: float,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
"""
|
||||
@@ -70,6 +71,7 @@ Currently, the arguments are:
|
||||
* `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space).
|
||||
* `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting.
|
||||
* `backtest_stats`: Backtesting statistics using the same format as the backtesting file "strategy" substructure. Available fields can be seen in `generate_strategy_stats()` in `optimize_reports.py`.
|
||||
* `starting_balance`: Starting balance used for backtesting.
|
||||
|
||||
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ This guide walks you through utilizing public trade data for advanced orderflow
|
||||
|
||||
!!! Warning "Experimental Feature"
|
||||
The orderflow feature is currently in beta and may be subject to changes in future releases. Please report any issues or feedback on the [Freqtrade GitHub repository](https://github.com/freqtrade/freqtrade/issues).
|
||||
It's also currently not been tested with freqAI - and combining these two features is considered out of scope at this point.
|
||||
|
||||
!!! Warning "Performance"
|
||||
Orderflow requires raw trades data. This data is rather large, and can cause a slow initial startup, when freqtrade needs to download the trades data for the last X candles. Additionally, enabling this feature will cause increased memory usage. Please ensure to have sufficient resources available.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
+29
-3
@@ -39,13 +39,19 @@ Please note that Environment variables will overwrite corresponding settings in
|
||||
|
||||
Common example:
|
||||
|
||||
```
|
||||
``` bash
|
||||
FREQTRADE__TELEGRAM__CHAT_ID=<telegramchatid>
|
||||
FREQTRADE__TELEGRAM__TOKEN=<telegramToken>
|
||||
FREQTRADE__EXCHANGE__KEY=<yourExchangeKey>
|
||||
FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
|
||||
```
|
||||
|
||||
Json lists are parsed as json - so you can use the following to set a list of pairs:
|
||||
|
||||
``` bash
|
||||
export FREQTRADE__EXCHANGE__PAIR_WHITELIST='["BTC/USDT", "ETH/USDT"]'
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable.
|
||||
|
||||
@@ -54,7 +60,7 @@ FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
|
||||
|
||||
??? Warning "Loading sequence"
|
||||
Environment variables are loaded after the initial configuration. As such, you cannot provide the path to the configuration through environment variables. Please use `--config path/to/config.json` for that.
|
||||
This also applies to user_dir to some degree. while the user directory can be set through environment variables - the configuration will **not** be loaded from that location.
|
||||
This also applies to `user_dir` to some degree. while the user directory can be set through environment variables - the configuration will **not** be loaded from that location.
|
||||
|
||||
### Multiple configuration files
|
||||
|
||||
@@ -168,7 +174,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `timeframe` | The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). Usually missing in configuration, and specified in the strategy. [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
||||
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
||||
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
|
||||
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode. [More information below](#dry-run-wallet)<br>*Defaults to `1000`.* <br> **Datatype:** Float or Dict
|
||||
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to exit a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||
@@ -225,6 +231,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
|
||||
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||
| `exchange.only_from_ccxt` | Prevent data-download from data.binance.vision. Leaving this as false can greatly speed up downloads, but may be problematic if the site is not available.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| | **Plugins**
|
||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
|
||||
@@ -323,6 +330,25 @@ To limit this calculation in case of large stoploss values, the calculated minim
|
||||
!!! Warning
|
||||
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. Freqtrade adjusts the stake-amount to this value, unless it's > 30% more than the calculated/desired stake-amount - in which case the trade is rejected.
|
||||
|
||||
#### Dry-run wallet
|
||||
|
||||
When running in dry-run mode, the bot will use a simulated wallet to execute trades. The starting balance of this wallet is defined by `dry_run_wallet` (defaults to 1000).
|
||||
For more complex scenarios, you can also assign a dictionary to `dry_run_wallet` to define the starting balance for each currency.
|
||||
|
||||
```json
|
||||
"dry_run_wallet": {
|
||||
"BTC": 0.01,
|
||||
"ETH": 2,
|
||||
"USDT": 1000
|
||||
}
|
||||
```
|
||||
|
||||
Command line options (`--dry-run-wallet`) can be used to override the configuration value, but only for the float value, not for the dictionary. If you'd like to use the dictionary, please adjust the configuration file.
|
||||
|
||||
!!! Note
|
||||
Balances not in stake-currency will not be used for trading, but are shown as part of the wallet balance.
|
||||
On Cross-margin exchanges, the wallet balance may be used to calculate the available collateral for trading.
|
||||
|
||||
#### Tradable balance
|
||||
|
||||
By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade.
|
||||
|
||||
@@ -40,6 +40,10 @@ This could be caused by the following reasons:
|
||||
* The installation did not complete successfully.
|
||||
* Please check the [Installation documentation](installation.md).
|
||||
|
||||
### The bot starts, but in STOPPED mode
|
||||
|
||||
Make sure you set the `initial_state` config option to `"running"` in your config.json
|
||||
|
||||
### I have waited 5 minutes, why hasn't the bot made any trades yet?
|
||||
|
||||
* Depending on the buy strategy, the amount of whitelisted coins, the
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
markdown==3.7
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.5.45
|
||||
mkdocs-material==9.5.48
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.12
|
||||
jinja2==3.1.4
|
||||
|
||||
+67
-53
@@ -88,8 +88,9 @@ Make sure that the following 2 lines are available in your docker-compose file:
|
||||
|
||||
### Consuming the API
|
||||
|
||||
You can consume the API by using `freqtrade-client` (also available as `scripts/rest_client.py`).
|
||||
This command can be installed independent of the bot by using `pip install freqtrade-client`.
|
||||
We advise consuming the API by using the supported `freqtrade-client` package (also available as `scripts/rest_client.py`).
|
||||
|
||||
This command can be installed independent of any running freqtrade bot by using `pip install freqtrade-client`.
|
||||
|
||||
This module is designed to be lightweight, and only depends on the `requests` and `python-rapidjson` modules, skipping all heavy dependencies freqtrade otherwise needs.
|
||||
|
||||
@@ -144,57 +145,6 @@ This method will work for all arguments - check the "show" command for a list of
|
||||
|
||||
For a full list of available commands, please refer to the list below.
|
||||
|
||||
### Available endpoints
|
||||
|
||||
| Command | Description |
|
||||
|----------|-------------|
|
||||
| `ping` | Simple command testing the API Readiness - requires no authentication.
|
||||
| `start` | Starts the trader.
|
||||
| `stop` | Stops the trader.
|
||||
| `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `reload_config` | Reloads the configuration file.
|
||||
| `trades` | List last trades. Limited to 500 trades per call.
|
||||
| `trade/<tradeid>` | Get specific trade.
|
||||
| `trades/<tradeid>` | DELETE - Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||
| `trades/<tradeid>/open-order` | DELETE - Cancel open order for this trade.
|
||||
| `trades/<tradeid>/reload` | GET - Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange.
|
||||
| `show_config` | Shows part of the current configuration with relevant settings to operation.
|
||||
| `logs` | Shows last log messages.
|
||||
| `status` | Lists all open trades.
|
||||
| `count` | Displays number of trades used and available.
|
||||
| `entries [pair]` | Shows profit statistics for each enter tags for given pair (or all pairs if pair isn't given). Pair is optional.
|
||||
| `exits [pair]` | Shows profit statistics for each exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.
|
||||
| `mix_tags [pair]` | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.
|
||||
| `locks` | Displays currently locked pairs.
|
||||
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
|
||||
| `locks add <pair>, <until>, [side], [reason]` | Locks a pair until "until". (Until will be rounded up to the nearest timeframe).
|
||||
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
|
||||
| `forceexit <trade_id> [order_type] [amount]` | Instantly exits the given trade (ignoring `minimum_roi`), using the given order type ("market" or "limit", uses your config setting if not specified), and the chosen amount (full sell if not specified).
|
||||
| `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
||||
| `forceenter <pair> [rate]` | Instantly enters the given pair. Rate is optional. (`force_entry_enable` must be set to True)
|
||||
| `forceenter <pair> <side> [rate]` | Instantly longs or shorts the given pair. Rate is optional. (`force_entry_enable` must be set to True)
|
||||
| `performance` | Show performance of each finished trade grouped by pair.
|
||||
| `balance` | Show account balance per currency.
|
||||
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
|
||||
| `weekly <n>` | Shows profit or loss per week, over the last n days (n defaults to 4).
|
||||
| `monthly <n>` | Shows profit or loss per month, over the last n days (n defaults to 3).
|
||||
| `stats` | Display a summary of profit / loss reasons as well as average holding times.
|
||||
| `whitelist` | Show the current whitelist.
|
||||
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||
| `edge` | Show validated pairs by Edge if it is enabled.
|
||||
| `pair_candles` | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
|
||||
| `pair_history` | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
|
||||
| `plot_config` | Get plot config from the strategy (or nothing if not configured). **Alpha**
|
||||
| `strategies` | List strategies in strategy directory. **Alpha**
|
||||
| `strategy <strategy>` | Get specific Strategy content. **Alpha**
|
||||
| `available_pairs` | List available backtest data. **Alpha**
|
||||
| `version` | Show version.
|
||||
| `sysinfo` | Show information about the system load.
|
||||
| `health` | Show bot health (last bot loop).
|
||||
|
||||
!!! Warning "Alpha status"
|
||||
Endpoints labeled with *Alpha status* above may change at any time without notice.
|
||||
|
||||
Possible commands can be listed from the rest-client script using the `help` command.
|
||||
|
||||
``` bash
|
||||
@@ -266,6 +216,14 @@ forceexit
|
||||
health
|
||||
Provides a quick health check of the running bot.
|
||||
|
||||
lock_add
|
||||
Manually lock a specific pair
|
||||
|
||||
:param pair: Pair to lock
|
||||
:param until: Lock until this date (format "2024-03-30 16:00:00Z")
|
||||
:param side: Side to lock (long, short, *)
|
||||
:param reason: Reason for the lock
|
||||
|
||||
locks
|
||||
Return current locks
|
||||
|
||||
@@ -353,6 +311,62 @@ whitelist
|
||||
|
||||
```
|
||||
|
||||
### Available endpoints
|
||||
|
||||
If you wish to call the REST API manually via another route, e.g. directly via `curl`, the table below shows the relevant URL endpoints and parameters.
|
||||
All endpoints in the below table need to be prefixed with the base URL of the API, e.g. `http://127.0.0.1:8080/api/v1/` - so the command becomes `http://127.0.0.1:8080/api/v1/<command>`.
|
||||
|
||||
| Endpoint | Method | Description / Parameters |
|
||||
|-----------|--------|--------------------------|
|
||||
| `/ping` | GET | Simple command testing the API Readiness - requires no authentication.
|
||||
| `/start` | POST | Starts the trader.
|
||||
| `/stop` | POST | Stops the trader.
|
||||
| `/stopbuy` | POST | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `/reload_config` | POST | Reloads the configuration file.
|
||||
| `/trades` | GET | List last trades. Limited to 500 trades per call.
|
||||
| `/trade/<tradeid>` | GET | Get specific trade.<br/>*Params:*<br/>- `tradeid` (`int`)
|
||||
| `/trades/<tradeid>` | DELETE | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.<br/>*Params:*<br/>- `tradeid` (`int`)
|
||||
| `/trades/<tradeid>/open-order` | DELETE | Cancel open order for this trade.<br/>*Params:*<br/>- `tradeid` (`int`)
|
||||
| `/trades/<tradeid>/reload` | POST | Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange.<br/>*Params:*<br/>- `tradeid` (`int`)
|
||||
| `/show_config` | GET | Shows part of the current configuration with relevant settings to operation.
|
||||
| `/logs` | GET | Shows last log messages.
|
||||
| `/status` | GET | Lists all open trades.
|
||||
| `/count` | GET | Displays number of trades used and available.
|
||||
| `/entries` | GET | Shows profit statistics for each enter tags for given pair (or all pairs if pair isn't given). Pair is optional.<br/>*Params:*<br/>- `pair` (`str`)
|
||||
| `/exits` | GET | Shows profit statistics for each exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.<br/>*Params:*<br/>- `pair` (`str`)
|
||||
| `/mix_tags` | GET | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.<br/>*Params:*<br/>- `pair` (`str`)
|
||||
| `/locks` | GET | Displays currently locked pairs.
|
||||
| `/locks` | POST | Locks a pair until "until". (Until will be rounded up to the nearest timeframe). Side is optional and is either `long` or `short` (default is `long`). Reason is optional.<br/>*Params:*<br/>- `<pair>` (`str`)<br/>- `<until>` (`datetime`)<br/>- `[side]` (`str`)<br/>- `[reason]` (`str`)
|
||||
| `/locks/<lockid>` | DELETE | Deletes (disables) the lock by id.<br/>*Params:*<br/>- `lockid` (`int`)
|
||||
| `/profit` | GET | Display a summary of your profit/loss from close trades and some stats about your performance.
|
||||
| `/forceexit` | POST | Instantly exits the given trade (ignoring `minimum_roi`), using the given order type ("market" or "limit", uses your config setting if not specified), and the chosen amount (full sell if not specified). If `all` is supplied as the `tradeid`, then all currently open trades will be forced to exit.<br/>*Params:*<br/>- `<tradeid>` (`int` or `str`)<br/>- `<ordertype>` (`str`)<br/>- `[amount]` (`float`)
|
||||
| `/forceenter` | POST | Instantly enters the given pair. Side is optional and is either `long` or `short` (default is `long`). Rate is optional. (`force_entry_enable` must be set to True)<br/>*Params:*<br/>- `<pair>` (`str`)<br/>- `<side>` (`str`)<br/>- `[rate]` (`float`)
|
||||
| `/performance` | GET | Show performance of each finished trade grouped by pair.
|
||||
| `/balance` | GET | Show account balance per currency.
|
||||
| `/daily` | GET | Shows profit or loss per day, over the last n days (n defaults to 7).<br/>*Params:*<br/>- `<n>` (`int`)
|
||||
| `/weekly` | GET | Shows profit or loss per week, over the last n days (n defaults to 4).<br/>*Params:*<br/>- `<n>` (`int`)
|
||||
| `/monthly` | GET | Shows profit or loss per month, over the last n days (n defaults to 3).<br/>*Params:*<br/>- `<n>` (`int`)
|
||||
| `/stats` | GET | Display a summary of profit / loss reasons as well as average holding times.
|
||||
| `/whitelist` | GET | Show the current whitelist.
|
||||
| `/blacklist` | GET | Show the current blacklist.
|
||||
| `/blacklist` | POST | Adds the specified pair to the blacklist.<br/>*Params:*<br/>- `pair` (`str`)
|
||||
| `/blacklist` | DELETE | Deletes the specified list of pairs from the blacklist.<br/>*Params:*<br/>- `[pair,pair]` (`list[str]`)
|
||||
| `/edge` | GET | Show validated pairs by Edge if it is enabled.
|
||||
| `/pair_candles` | GET | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
|
||||
| `/pair_candles` | POST | Returns dataframe for a pair / timeframe combination while the bot is running, filtered by a provided list of columns to return. **Alpha**<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
|
||||
| `/pair_history` | GET | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
|
||||
| `/pair_history` | POST | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy, filtered by a provided list of columns to return. **Alpha**<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
|
||||
| `/plot_config` | GET | Get plot config from the strategy (or nothing if not configured). **Alpha**
|
||||
| `/strategies` | GET | List strategies in strategy directory. **Alpha**
|
||||
| `/strategy/<strategy>` | GET | Get specific Strategy content by strategy class name. **Alpha**<br/>*Params:*<br/>- `<strategy>` (`str`)
|
||||
| `/available_pairs` | GET | List available backtest data. **Alpha**
|
||||
| `/version` | GET | Show version.
|
||||
| `/sysinfo` | GET | Show information about the system load.
|
||||
| `/health` | GET | Show bot health (last bot loop).
|
||||
|
||||
!!! Warning "Alpha status"
|
||||
Endpoints labeled with *Alpha status* above may change at any time without notice.
|
||||
|
||||
### Message WebSocket
|
||||
|
||||
The API Server includes a websocket endpoint for subscribing to RPC messages from the freqtrade Bot.
|
||||
|
||||
@@ -767,6 +767,15 @@ Adjustment orders can be assigned with a tag by returning a 2 element Tuple, wit
|
||||
|
||||
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
|
||||
|
||||
!!! Danger "Loose Logic"
|
||||
On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, for example your logic for extra entry is only to check RSI of last candle is below 30, then when such condition fulfilled, your bot will do extra re-entry every 5 secs until either it run out of money, it hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived.
|
||||
|
||||
Same thing also can happen with partial exit. So be sure to have a strict logic and/or check for the last filled order.
|
||||
|
||||
!!! Warning "Backtesting"
|
||||
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
|
||||
This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle.
|
||||
|
||||
### Increase position
|
||||
|
||||
The strategy is expected to return a positive **stake_amount** (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades).
|
||||
@@ -776,16 +785,22 @@ If there are not enough funds in the wallet (the return value is above `max_stak
|
||||
|
||||
Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
|
||||
|
||||
!!! Note "About stake size"
|
||||
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
||||
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
|
||||
Using `"unlimited"` stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order.
|
||||
|
||||
### Decrease position
|
||||
|
||||
The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit.
|
||||
Returning the full owned stake at that point (`-trade.stake_amount`) results in a full exit.
|
||||
Returning a value more than the above (so remaining stake_amount would become negative) will result in the bot ignoring the signal.
|
||||
|
||||
!!! Note "About stake size"
|
||||
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
||||
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
|
||||
Using `"unlimited"` stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order.
|
||||
For a partial exit, it's important to know that the formula used to calculate the amount of the coin for the partial exit order is `amount to be exited partially = negative_stake_amount * trade.amount / trade.stake_amount`, where `negative_stake_amount` is the value returned from the `adjust_trade_position` function. As seen in the formula, the formula doesn't care about current profit/loss of the position. It only cares about `trade.amount` and `trade.stake_amount` which aren't affected by the price movement at all.
|
||||
|
||||
For example, let's say you buy 2 SHITCOIN/USDT at open rate of 50, which means the trade's stake amount is 100 USDT. Now the price raises to 200 and you want to sell half of it. In that case, you have to return -50% of `trade.stake_amount` (0.5 * 100 USDT) which equals to -50. The bot will calculate the amount it needed to sell, which is `50 * 2 / 100` which equals 1 SHITCOIN/USDT. If you return -200 (50% of 2 * 200), the bot will ignore it since `trade.stake_amount` is only 100 USDT but you asked to sell 200 USDT which means you are asking to sell 4 SHITCOIN/USDT.
|
||||
|
||||
Back to the example above, since current rate is 200, the current USDT value of your trade is now 400 USDT. Let's say you want to partially sell 100 USDT to take out the initial investment and leave the profit in the trade hoping that the price keeps rising. In that case, you have to do a different approach. First, you need to calculate the exact amount you needed to sell. In this case, since you want to sell 100 USDT worth based of current rate, the exact amount you need to partially sell is `100 * 2 / 400` which equals 0.5 SHITCOIN/USDT. Since we know now the exact amount we want to sell (0.5), the value you need to return in the `adjust_trade_position` function is `-amount to be exited partially * trade.stake_amount / trade.amount`, which equals -25. The bot will sell 0.5 SHITCOIN/USDT, keeping 1.5 in trade. You will receive 100 USDT from the partial exit.
|
||||
|
||||
!!! Warning "Stoploss calculation"
|
||||
Stoploss is still calculated from the initial opening price, not averaged price.
|
||||
@@ -793,10 +808,6 @@ Returning a value more than the above (so remaining stake_amount would become ne
|
||||
|
||||
While `/stopentry` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades.
|
||||
|
||||
!!! Warning "Backtesting"
|
||||
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
|
||||
This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle.
|
||||
|
||||
!!! Warning "Performance with many position adjustments"
|
||||
Position adjustments can be a good approach to increase a strategy's output - but it can also have drawbacks if using this feature extensively.
|
||||
Each of the orders will be attached to the trade object for the duration of the trade - hence increasing memory usage.
|
||||
|
||||
@@ -152,7 +152,7 @@ print(stats["strategy"][strategy]["pairlist"])
|
||||
# Get market change (average change of all pairs from start to end of the backtest period)
|
||||
print(stats["strategy"][strategy]["market_change"])
|
||||
# Maximum drawdown ()
|
||||
print(stats["strategy"][strategy]["max_drawdown"])
|
||||
print(stats["strategy"][strategy]["max_drawdown_abs"])
|
||||
# Maximum drawdown start and end
|
||||
print(stats["strategy"][strategy]["drawdown_start"])
|
||||
print(stats["strategy"][strategy]["drawdown_end"])
|
||||
|
||||
@@ -15,3 +15,7 @@
|
||||
.md-version__list {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
#available-endpoints ~ .md-typeset__scrollwrap .md-typeset__table th:first-of-type {
|
||||
width: 35% !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2024.11-dev"
|
||||
__version__ = "2024.12-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -17,7 +17,7 @@ def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[
|
||||
:return: Configuration
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.util import fmt_coin
|
||||
from freqtrade.util import fmt_coin, get_dry_run_wallet
|
||||
|
||||
config = setup_utils_configuration(args, method)
|
||||
|
||||
@@ -26,7 +26,7 @@ def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[
|
||||
RunMode.HYPEROPT: "hyperoptimization",
|
||||
}
|
||||
if method in no_unlimited_runmodes.keys():
|
||||
wallet_size = config["dry_run_wallet"] * config["tradable_balance_ratio"]
|
||||
wallet_size = get_dry_run_wallet(config) * config["tradable_balance_ratio"]
|
||||
# tradable_balance_ratio
|
||||
if (
|
||||
config["stake_amount"] != constants.UNLIMITED_STAKE_AMOUNT
|
||||
|
||||
@@ -85,8 +85,10 @@ CONF_SCHEMA = {
|
||||
},
|
||||
"dry_run_wallet": {
|
||||
"description": "Initial wallet balance for dry run mode.",
|
||||
"type": "number",
|
||||
"type": ["number", "object"],
|
||||
"default": DRY_RUN_WALLET,
|
||||
"patternProperties": {r"^[a-zA-Z0-9]+$": {"type": "number"}},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
"cancel_open_orders_on_exit": {
|
||||
"description": "Cancel open orders when exiting.",
|
||||
|
||||
@@ -2,6 +2,8 @@ import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import rapidjson
|
||||
|
||||
from freqtrade.constants import ENV_VAR_PREFIX
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
|
||||
@@ -20,6 +22,14 @@ def _get_var_typed(val):
|
||||
return True
|
||||
elif val.lower() in ("f", "false"):
|
||||
return False
|
||||
# try to convert from json
|
||||
try:
|
||||
value = rapidjson.loads(val)
|
||||
# Limited to lists for now
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
except rapidjson.JSONDecodeError:
|
||||
pass
|
||||
# keep as string
|
||||
return val
|
||||
|
||||
|
||||
@@ -4,20 +4,31 @@ Functions to convert orderflow data from public_trades
|
||||
|
||||
import logging
|
||||
import time
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS, Config
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import DependencyException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ORDERFLOW_ADDED_COLUMNS = [
|
||||
"trades",
|
||||
"orderflow",
|
||||
"imbalances",
|
||||
"stacked_imbalances_bid",
|
||||
"stacked_imbalances_ask",
|
||||
"max_delta",
|
||||
"min_delta",
|
||||
"bid",
|
||||
"ask",
|
||||
"delta",
|
||||
"total_trades",
|
||||
]
|
||||
|
||||
|
||||
def _init_dataframe_with_trades_columns(dataframe: pd.DataFrame):
|
||||
"""
|
||||
@@ -25,53 +36,73 @@ def _init_dataframe_with_trades_columns(dataframe: pd.DataFrame):
|
||||
:param dataframe: Dataframe to populate
|
||||
"""
|
||||
# Initialize columns with appropriate dtypes
|
||||
dataframe["trades"] = np.nan
|
||||
dataframe["orderflow"] = np.nan
|
||||
dataframe["imbalances"] = np.nan
|
||||
dataframe["stacked_imbalances_bid"] = np.nan
|
||||
dataframe["stacked_imbalances_ask"] = np.nan
|
||||
dataframe["max_delta"] = np.nan
|
||||
dataframe["min_delta"] = np.nan
|
||||
dataframe["bid"] = np.nan
|
||||
dataframe["ask"] = np.nan
|
||||
dataframe["delta"] = np.nan
|
||||
dataframe["total_trades"] = np.nan
|
||||
for column in ORDERFLOW_ADDED_COLUMNS:
|
||||
dataframe[column] = np.nan
|
||||
|
||||
# Ensure the 'trades' column is of object type
|
||||
dataframe["trades"] = dataframe["trades"].astype(object)
|
||||
dataframe["orderflow"] = dataframe["orderflow"].astype(object)
|
||||
dataframe["imbalances"] = dataframe["imbalances"].astype(object)
|
||||
dataframe["stacked_imbalances_bid"] = dataframe["stacked_imbalances_bid"].astype(object)
|
||||
dataframe["stacked_imbalances_ask"] = dataframe["stacked_imbalances_ask"].astype(object)
|
||||
# Set columns to object type
|
||||
for column in (
|
||||
"trades",
|
||||
"orderflow",
|
||||
"imbalances",
|
||||
"stacked_imbalances_bid",
|
||||
"stacked_imbalances_ask",
|
||||
):
|
||||
dataframe[column] = dataframe[column].astype(object)
|
||||
|
||||
|
||||
def timeframe_to_DateOffset(timeframe: str) -> pd.DateOffset:
|
||||
"""
|
||||
Translates the timeframe interval value written in the human readable
|
||||
form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
|
||||
of seconds for one timeframe interval.
|
||||
"""
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
|
||||
timeframe_seconds = timeframe_to_seconds(timeframe)
|
||||
timeframe_minutes = timeframe_seconds // 60
|
||||
if timeframe_minutes < 1:
|
||||
return pd.DateOffset(seconds=timeframe_seconds)
|
||||
elif 59 < timeframe_minutes < 1440:
|
||||
return pd.DateOffset(hours=timeframe_minutes // 60)
|
||||
elif 1440 <= timeframe_minutes < 10080:
|
||||
return pd.DateOffset(days=timeframe_minutes // 1440)
|
||||
elif 10000 < timeframe_minutes < 43200:
|
||||
return pd.DateOffset(weeks=1)
|
||||
elif timeframe_minutes >= 43200 and timeframe_minutes < 525600:
|
||||
return pd.DateOffset(months=1)
|
||||
elif timeframe == "1y":
|
||||
return pd.DateOffset(years=1)
|
||||
else:
|
||||
return pd.DateOffset(minutes=timeframe_minutes)
|
||||
|
||||
|
||||
def _calculate_ohlcv_candle_start_and_end(df: pd.DataFrame, timeframe: str):
|
||||
from freqtrade.exchange import timeframe_to_next_date, timeframe_to_resample_freq
|
||||
from freqtrade.exchange import timeframe_to_resample_freq
|
||||
|
||||
timeframe_frequency = timeframe_to_resample_freq(timeframe)
|
||||
# calculate ohlcv candle start and end
|
||||
if df is not None and not df.empty:
|
||||
timeframe_frequency = timeframe_to_resample_freq(timeframe)
|
||||
dofs = timeframe_to_DateOffset(timeframe)
|
||||
# calculate ohlcv candle start and end
|
||||
df["datetime"] = pd.to_datetime(df["date"], unit="ms")
|
||||
df["candle_start"] = df["datetime"].dt.floor(timeframe_frequency)
|
||||
# used in _now_is_time_to_refresh_trades
|
||||
df["candle_end"] = df["candle_start"].apply(
|
||||
lambda candle_start: timeframe_to_next_date(timeframe, candle_start)
|
||||
)
|
||||
df["candle_end"] = df["candle_start"] + dofs
|
||||
df.drop(columns=["datetime"], inplace=True)
|
||||
|
||||
|
||||
def populate_dataframe_with_trades(
|
||||
cached_grouped_trades: OrderedDict[tuple[datetime, datetime], pd.DataFrame],
|
||||
cached_grouped_trades: pd.DataFrame | None,
|
||||
config: Config,
|
||||
dataframe: pd.DataFrame,
|
||||
trades: pd.DataFrame,
|
||||
) -> tuple[pd.DataFrame, OrderedDict[tuple[datetime, datetime], pd.DataFrame]]:
|
||||
) -> tuple[pd.DataFrame, pd.DataFrame]:
|
||||
"""
|
||||
Populates a dataframe with trades
|
||||
:param dataframe: Dataframe to populate
|
||||
:param trades: Trades to populate with
|
||||
:return: Dataframe with trades populated
|
||||
"""
|
||||
|
||||
timeframe = config["timeframe"]
|
||||
config_orderflow = config["orderflow"]
|
||||
|
||||
@@ -94,71 +125,52 @@ def populate_dataframe_with_trades(
|
||||
|
||||
# group trades by candle start
|
||||
trades_grouped_by_candle_start = trades.groupby("candle_start", group_keys=False)
|
||||
# Create Series to hold complex data
|
||||
trades_series = pd.Series(index=dataframe.index, dtype=object)
|
||||
orderflow_series = pd.Series(index=dataframe.index, dtype=object)
|
||||
imbalances_series = pd.Series(index=dataframe.index, dtype=object)
|
||||
stacked_imbalances_bid_series = pd.Series(index=dataframe.index, dtype=object)
|
||||
stacked_imbalances_ask_series = pd.Series(index=dataframe.index, dtype=object)
|
||||
|
||||
trades_grouped_by_candle_start = trades.groupby("candle_start", group_keys=False)
|
||||
candle_start: datetime
|
||||
for candle_start, trades_grouped_df in trades_grouped_by_candle_start:
|
||||
is_between = candle_start == dataframe["date"]
|
||||
if is_between.any():
|
||||
from freqtrade.exchange import timeframe_to_next_date
|
||||
# there can only be one row with the same date
|
||||
index = dataframe.index[is_between][0]
|
||||
|
||||
candle_next = timeframe_to_next_date(timeframe, typing.cast(datetime, candle_start))
|
||||
if candle_next not in trades_grouped_by_candle_start.groups:
|
||||
logger.warning(
|
||||
f"candle at {candle_start} with {len(trades_grouped_df)} trades "
|
||||
f"might be unfinished, because no finished trades at {candle_next}"
|
||||
)
|
||||
|
||||
indices = dataframe.index[is_between].tolist()
|
||||
# Add trades to each candle
|
||||
trades_series.loc[indices] = [
|
||||
trades_grouped_df.drop(columns=["candle_start", "candle_end"]).to_dict(
|
||||
orient="records"
|
||||
)
|
||||
]
|
||||
# Use caching mechanism
|
||||
if (candle_start, candle_next) in cached_grouped_trades:
|
||||
cache_entry = cached_grouped_trades[
|
||||
(typing.cast(datetime, candle_start), candle_next)
|
||||
]
|
||||
# dataframe.loc[is_between] = cache_entry # doesn't take, so we need workaround:
|
||||
# Create a dictionary of the column values to be assigned
|
||||
update_dict = {c: cache_entry[c].iat[0] for c in cache_entry.columns}
|
||||
# Assign the values using the update_dict
|
||||
dataframe.loc[is_between, update_dict.keys()] = pd.DataFrame(
|
||||
[update_dict], index=dataframe.loc[is_between].index
|
||||
)
|
||||
if (
|
||||
cached_grouped_trades is not None
|
||||
and (candle_start == cached_grouped_trades["date"]).any()
|
||||
):
|
||||
# Check if the trades are already in the cache
|
||||
cache_idx = cached_grouped_trades.index[
|
||||
cached_grouped_trades["date"] == candle_start
|
||||
][0]
|
||||
for col in ORDERFLOW_ADDED_COLUMNS:
|
||||
dataframe.at[index, col] = cached_grouped_trades.at[cache_idx, col]
|
||||
continue
|
||||
|
||||
dataframe.at[index, "trades"] = trades_grouped_df.drop(
|
||||
columns=["candle_start", "candle_end"]
|
||||
).to_dict(orient="records")
|
||||
|
||||
# Calculate orderflow for each candle
|
||||
orderflow = trades_to_volumeprofile_with_total_delta_bid_ask(
|
||||
trades_grouped_df, scale=config_orderflow["scale"]
|
||||
)
|
||||
orderflow_series.loc[indices] = [orderflow.to_dict(orient="index")]
|
||||
dataframe.at[index, "orderflow"] = orderflow.to_dict(orient="index")
|
||||
# orderflow_series.loc[[index]] = [orderflow.to_dict(orient="index")]
|
||||
# Calculate imbalances for each candle's orderflow
|
||||
imbalances = trades_orderflow_to_imbalances(
|
||||
orderflow,
|
||||
imbalance_ratio=config_orderflow["imbalance_ratio"],
|
||||
imbalance_volume=config_orderflow["imbalance_volume"],
|
||||
)
|
||||
imbalances_series.loc[indices] = [imbalances.to_dict(orient="index")]
|
||||
dataframe.at[index, "imbalances"] = imbalances.to_dict(orient="index")
|
||||
|
||||
stacked_imbalance_range = config_orderflow["stacked_imbalance_range"]
|
||||
stacked_imbalances_bid_series.loc[indices] = [
|
||||
stacked_imbalance_bid(
|
||||
imbalances, stacked_imbalance_range=stacked_imbalance_range
|
||||
)
|
||||
]
|
||||
stacked_imbalances_ask_series.loc[indices] = [
|
||||
stacked_imbalance_ask(
|
||||
imbalances, stacked_imbalance_range=stacked_imbalance_range
|
||||
)
|
||||
]
|
||||
dataframe.at[index, "stacked_imbalances_bid"] = stacked_imbalance_bid(
|
||||
imbalances, stacked_imbalance_range=stacked_imbalance_range
|
||||
)
|
||||
|
||||
dataframe.at[index, "stacked_imbalances_ask"] = stacked_imbalance_ask(
|
||||
imbalances, stacked_imbalance_range=stacked_imbalance_range
|
||||
)
|
||||
|
||||
bid = np.where(
|
||||
trades_grouped_df["side"].str.contains("sell"), trades_grouped_df["amount"], 0
|
||||
@@ -168,39 +180,20 @@ def populate_dataframe_with_trades(
|
||||
trades_grouped_df["side"].str.contains("buy"), trades_grouped_df["amount"], 0
|
||||
)
|
||||
deltas_per_trade = ask - bid
|
||||
min_delta = deltas_per_trade.cumsum().min()
|
||||
max_delta = deltas_per_trade.cumsum().max()
|
||||
dataframe.loc[indices, "max_delta"] = max_delta
|
||||
dataframe.loc[indices, "min_delta"] = min_delta
|
||||
dataframe.at[index, "max_delta"] = deltas_per_trade.cumsum().max()
|
||||
dataframe.at[index, "min_delta"] = deltas_per_trade.cumsum().min()
|
||||
|
||||
dataframe.loc[indices, "bid"] = bid.sum()
|
||||
dataframe.loc[indices, "ask"] = ask.sum()
|
||||
dataframe.loc[indices, "delta"] = (
|
||||
dataframe.loc[indices, "ask"] - dataframe.loc[indices, "bid"]
|
||||
dataframe.at[index, "bid"] = bid.sum()
|
||||
dataframe.at[index, "ask"] = ask.sum()
|
||||
dataframe.at[index, "delta"] = (
|
||||
dataframe.at[index, "ask"] - dataframe.at[index, "bid"]
|
||||
)
|
||||
dataframe.loc[indices, "total_trades"] = len(trades_grouped_df)
|
||||
dataframe.at[index, "total_trades"] = len(trades_grouped_df)
|
||||
|
||||
# Cache the result
|
||||
cached_grouped_trades[(typing.cast(datetime, candle_start), candle_next)] = (
|
||||
dataframe.loc[is_between].copy()
|
||||
)
|
||||
|
||||
# Maintain cache size
|
||||
if (
|
||||
config.get("runmode") in (RunMode.DRY_RUN, RunMode.LIVE)
|
||||
and len(cached_grouped_trades) > config_orderflow["cache_size"]
|
||||
):
|
||||
cached_grouped_trades.popitem(last=False)
|
||||
else:
|
||||
logger.debug(f"Found NO candles for trades starting with {candle_start}")
|
||||
logger.debug(f"trades.groups_keys in {time.time() - start_time} seconds")
|
||||
|
||||
# Merge the complex data Series back into the DataFrame
|
||||
dataframe["trades"] = trades_series
|
||||
dataframe["orderflow"] = orderflow_series
|
||||
dataframe["imbalances"] = imbalances_series
|
||||
dataframe["stacked_imbalances_bid"] = stacked_imbalances_bid_series
|
||||
dataframe["stacked_imbalances_ask"] = stacked_imbalances_ask_series
|
||||
# Cache the entire dataframe
|
||||
cached_grouped_trades = dataframe.tail(config_orderflow["cache_size"]).copy()
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Error populating dataframe with trades")
|
||||
|
||||
@@ -285,6 +285,7 @@ def _download_pair_history(
|
||||
candle_type=candle_type,
|
||||
until_ms=until_ms if until_ms else None,
|
||||
)
|
||||
logger.info(f"Downloaded data for {pair} with length {len(new_dataframe)}.")
|
||||
if data.empty:
|
||||
data = new_dataframe
|
||||
else:
|
||||
@@ -603,7 +604,7 @@ def download_data(
|
||||
Download data function. Used from both cli and API.
|
||||
"""
|
||||
timerange = TimeRange()
|
||||
if "days" in config:
|
||||
if "days" in config and config["days"] is not None:
|
||||
time_since = (datetime.now() - timedelta(days=config["days"])).strftime("%Y%m%d")
|
||||
timerange = TimeRange.parse_timerange(f"{time_since}-")
|
||||
|
||||
|
||||
+103
-16
@@ -5,13 +5,18 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import ccxt
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.binance_public_data import concat_safe, download_archive_ohlcv
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import FtHas, OHLCVResponse, Tickers
|
||||
from freqtrade.exchange.exchange_types import FtHas, Tickers
|
||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -52,8 +57,14 @@ class Binance(Exchange):
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||
]
|
||||
|
||||
def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers:
|
||||
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
||||
def get_tickers(
|
||||
self,
|
||||
symbols: list[str] | None = None,
|
||||
*,
|
||||
cached: bool = False,
|
||||
market_type: TradingMode | None = None,
|
||||
) -> Tickers:
|
||||
tickers = super().get_tickers(symbols=symbols, cached=cached, market_type=market_type)
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
# Binance's future result has no bid/ask values.
|
||||
# Therefore we must fetch that from fetch_bids_asks and combine the two results.
|
||||
@@ -80,7 +91,10 @@ class Binance(Exchange):
|
||||
"\nHedge Mode is not supported by freqtrade. "
|
||||
"Please change 'Position Mode' on your binance futures account."
|
||||
)
|
||||
if assets_margin.get("multiAssetsMargin") is True:
|
||||
if (
|
||||
assets_margin.get("multiAssetsMargin") is True
|
||||
and self.margin_mode != MarginMode.CROSS
|
||||
):
|
||||
msg += (
|
||||
"\nMulti-Asset Mode is not supported by freqtrade. "
|
||||
"Please change 'Asset Mode' on your binance futures account."
|
||||
@@ -97,23 +111,24 @@ class Binance(Exchange):
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
async def _async_get_historic_ohlcv(
|
||||
def get_historic_ohlcv(
|
||||
self,
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
since_ms: int,
|
||||
candle_type: CandleType,
|
||||
is_new_pair: bool = False,
|
||||
raise_: bool = False,
|
||||
until_ms: int | None = None,
|
||||
) -> OHLCVResponse:
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
||||
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
"""
|
||||
if is_new_pair:
|
||||
x = await self._async_get_candle_history(pair, timeframe, candle_type, 0)
|
||||
x = self.loop.run_until_complete(
|
||||
self._async_get_candle_history(pair, timeframe, candle_type, 0)
|
||||
)
|
||||
if x and x[3] and x[3][0] and x[3][0][0] > since_ms:
|
||||
# Set starting date to first available candle.
|
||||
since_ms = x[3][0][0]
|
||||
@@ -121,17 +136,89 @@ class Binance(Exchange):
|
||||
f"Candle-data for {pair} available starting with "
|
||||
f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}."
|
||||
)
|
||||
if until_ms and since_ms >= until_ms:
|
||||
logger.warning(
|
||||
f"No available candle-data for {pair} before "
|
||||
f"{dt_from_ts(until_ms).isoformat()}"
|
||||
)
|
||||
return DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
|
||||
|
||||
return await super()._async_get_historic_ohlcv(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms,
|
||||
is_new_pair=is_new_pair,
|
||||
raise_=raise_,
|
||||
candle_type=candle_type,
|
||||
until_ms=until_ms,
|
||||
if (
|
||||
self._config["exchange"].get("only_from_ccxt", False)
|
||||
or
|
||||
# only download timeframes with significant improvements,
|
||||
# otherwise fall back to rest API
|
||||
not (
|
||||
(candle_type == CandleType.SPOT and timeframe in ["1s", "1m", "3m", "5m"])
|
||||
or (
|
||||
candle_type == CandleType.FUTURES
|
||||
and timeframe in ["1m", "3m", "5m", "15m", "30m"]
|
||||
)
|
||||
)
|
||||
):
|
||||
return super().get_historic_ohlcv(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms,
|
||||
candle_type=candle_type,
|
||||
is_new_pair=is_new_pair,
|
||||
until_ms=until_ms,
|
||||
)
|
||||
else:
|
||||
# Download from data.binance.vision
|
||||
return self.get_historic_ohlcv_fast(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms,
|
||||
candle_type=candle_type,
|
||||
is_new_pair=is_new_pair,
|
||||
until_ms=until_ms,
|
||||
)
|
||||
|
||||
def get_historic_ohlcv_fast(
|
||||
self,
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
since_ms: int,
|
||||
candle_type: CandleType,
|
||||
is_new_pair: bool = False,
|
||||
until_ms: int | None = None,
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Fastly fetch OHLCV data by leveraging https://data.binance.vision.
|
||||
"""
|
||||
df = self.loop.run_until_complete(
|
||||
download_archive_ohlcv(
|
||||
candle_type=candle_type,
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms,
|
||||
until_ms=until_ms,
|
||||
markets=self.markets,
|
||||
)
|
||||
)
|
||||
|
||||
# download the remaining data from rest API
|
||||
if df.empty:
|
||||
rest_since_ms = since_ms
|
||||
else:
|
||||
rest_since_ms = dt_ts(df.iloc[-1].date) + timeframe_to_msecs(timeframe)
|
||||
|
||||
# make sure since <= until
|
||||
if until_ms and rest_since_ms > until_ms:
|
||||
rest_df = DataFrame()
|
||||
else:
|
||||
rest_df = super().get_historic_ohlcv(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=rest_since_ms,
|
||||
candle_type=candle_type,
|
||||
is_new_pair=is_new_pair,
|
||||
until_ms=until_ms,
|
||||
)
|
||||
all_df = concat_safe([df, rest_df])
|
||||
return all_df
|
||||
|
||||
def funding_fee_cutoff(self, open_date: datetime):
|
||||
"""
|
||||
Funding fees are only charged at full hours (usually every 4-8h).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,281 @@
|
||||
"""
|
||||
Fetch daily-archived OHLCV data from https://data.binance.vision/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import zipfile
|
||||
from datetime import date, timedelta
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.misc import chunks
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts, dt_now
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Http404(Exception):
|
||||
def __init__(self, msg, date, url):
|
||||
super().__init__(msg)
|
||||
self.date = date
|
||||
self.url = url
|
||||
|
||||
|
||||
class BadHttpStatus(Exception):
|
||||
"""Not 200/404"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
async def download_archive_ohlcv(
|
||||
candle_type: CandleType,
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
*,
|
||||
since_ms: int,
|
||||
until_ms: int | None,
|
||||
markets: dict[str, Any],
|
||||
stop_on_404: bool = True,
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Fetch OHLCV data from https://data.binance.vision
|
||||
The function makes its best effort to download data within the time range
|
||||
[`since_ms`, `until_ms`] -- including `since_ms`, but excluding `until_ms`.
|
||||
If `stop_one_404` is True, this returned DataFrame is guaranteed to start from `since_ms`
|
||||
with no gaps in the data.
|
||||
|
||||
:candle_type: Currently only spot and futures are supported
|
||||
:pair: symbol name in CCXT convention
|
||||
:since_ms: the start timestamp of data, including itself
|
||||
:until_ms: the end timestamp of data, excluding itself
|
||||
:param until_ms: `None` indicates the timestamp of the latest available data
|
||||
:markets: the CCXT markets dict, when it's None, the function will load the markets data
|
||||
from a new `ccxt.binance` instance
|
||||
:param stop_on_404: Stop to download the following data when a 404 returned
|
||||
:return: the date range is between [since_ms, until_ms), return an empty DataFrame if no data
|
||||
available in the time range
|
||||
"""
|
||||
try:
|
||||
if candle_type == CandleType.SPOT:
|
||||
asset_type_url_segment = "spot"
|
||||
elif candle_type == CandleType.FUTURES:
|
||||
asset_type_url_segment = "futures/um"
|
||||
else:
|
||||
raise ValueError(f"Unsupported CandleType: {candle_type}")
|
||||
|
||||
symbol = markets[pair]["id"]
|
||||
|
||||
start = dt_from_ts(since_ms)
|
||||
end = dt_from_ts(until_ms) if until_ms else dt_now()
|
||||
|
||||
# We use two days ago as the last available day because the daily archives are daily
|
||||
# uploaded and have several hours delay
|
||||
last_available_date = dt_now() - timedelta(days=2)
|
||||
end = min(end, last_available_date)
|
||||
if start >= end:
|
||||
return DataFrame()
|
||||
df = await _download_archive_ohlcv(
|
||||
asset_type_url_segment, symbol, pair, timeframe, start, end, stop_on_404
|
||||
)
|
||||
logger.debug(
|
||||
f"Downloaded data for {pair} from https://data.binance.vision with length {len(df)}."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"An exception occurred during fast download from Binance, falling back to "
|
||||
"the slower REST API, this can take more time.",
|
||||
exc_info=e,
|
||||
)
|
||||
df = DataFrame()
|
||||
|
||||
if not df.empty:
|
||||
# only return the data within the requested time range
|
||||
return df.loc[(df["date"] >= start) & (df["date"] < end)]
|
||||
else:
|
||||
return df
|
||||
|
||||
|
||||
def concat_safe(dfs) -> DataFrame:
|
||||
if all(df is None for df in dfs):
|
||||
return DataFrame()
|
||||
else:
|
||||
return pd.concat(dfs)
|
||||
|
||||
|
||||
async def _download_archive_ohlcv(
|
||||
asset_type_url_segment: str,
|
||||
symbol: str,
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
start: date,
|
||||
end: date,
|
||||
stop_on_404: bool,
|
||||
) -> DataFrame:
|
||||
# daily dataframes, `None` indicates missing data in that day (when `stop_on_404` is False)
|
||||
dfs: list[DataFrame | None] = []
|
||||
# the current day being processing, starting at 1.
|
||||
current_day = 0
|
||||
|
||||
connector = aiohttp.TCPConnector(limit=100)
|
||||
async with aiohttp.ClientSession(connector=connector, trust_env=True) as session:
|
||||
# the HTTP connections has been throttled by TCPConnector
|
||||
for dates in chunks(list(date_range(start, end)), 1000):
|
||||
tasks = [
|
||||
asyncio.create_task(
|
||||
get_daily_ohlcv(asset_type_url_segment, symbol, timeframe, date, session)
|
||||
)
|
||||
for date in dates
|
||||
]
|
||||
for task in tasks:
|
||||
current_day += 1
|
||||
try:
|
||||
df = await task
|
||||
except Http404 as e:
|
||||
if stop_on_404:
|
||||
logger.debug(f"Failed to download {e.url} due to 404.")
|
||||
|
||||
# A 404 error on the first day indicates missing data
|
||||
# on https://data.binance.vision, we provide the warning and the advice.
|
||||
# https://github.com/freqtrade/freqtrade/blob/acc53065e5fa7ab5197073276306dc9dc3adbfa3/tests/exchange_online/test_binance_compare_ohlcv.py#L7
|
||||
if current_day == 1:
|
||||
logger.warning(
|
||||
f"Fast download is unavailable due to missing data: "
|
||||
f"{e.url}. Falling back to the slower REST API, "
|
||||
"which may take more time."
|
||||
)
|
||||
if pair in ["BTC/USDT:USDT", "ETH/USDT:USDT", "BCH/USDT:USDT"]:
|
||||
logger.warning(
|
||||
f"To avoid the delay, you can first download {pair} using "
|
||||
"`--timerange <start date>-20200101`, and then download the "
|
||||
"remaining data with `--timerange 20200101-<end date>`."
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Binance fast download for {pair} stopped at {e.date} due to "
|
||||
f"missing data: {e.url}, falling back to rest API for the "
|
||||
"remaining data, this can take more time."
|
||||
)
|
||||
await cancel_and_await_tasks(tasks[tasks.index(task) + 1 :])
|
||||
return concat_safe(dfs)
|
||||
else:
|
||||
dfs.append(None)
|
||||
except BaseException as e:
|
||||
logger.warning(f"An exception raised: : {e}")
|
||||
# Directly return the existing data, do not allow the gap within the data
|
||||
await cancel_and_await_tasks(tasks[tasks.index(task) + 1 :])
|
||||
return concat_safe(dfs)
|
||||
else:
|
||||
dfs.append(df)
|
||||
return concat_safe(dfs)
|
||||
|
||||
|
||||
async def cancel_and_await_tasks(unawaited_tasks):
|
||||
"""Cancel and await the tasks"""
|
||||
logger.debug("Try to cancel uncompleted download tasks.")
|
||||
for task in unawaited_tasks:
|
||||
task.cancel()
|
||||
await asyncio.gather(*unawaited_tasks, return_exceptions=True)
|
||||
logger.debug("All download tasks were awaited.")
|
||||
|
||||
|
||||
def date_range(start: date, end: date):
|
||||
date = start
|
||||
while date <= end:
|
||||
yield date
|
||||
date += timedelta(days=1)
|
||||
|
||||
|
||||
def binance_vision_zip_name(symbol: str, timeframe: str, date: date) -> str:
|
||||
return f"{symbol}-{timeframe}-{date.strftime('%Y-%m-%d')}.zip"
|
||||
|
||||
|
||||
def binance_vision_zip_url(
|
||||
asset_type_url_segment: str, symbol: str, timeframe: str, date: date
|
||||
) -> str:
|
||||
"""
|
||||
example urls:
|
||||
https://data.binance.vision/data/spot/daily/klines/BTCUSDT/1s/BTCUSDT-1s-2023-10-27.zip
|
||||
https://data.binance.vision/data/futures/um/daily/klines/BTCUSDT/1h/BTCUSDT-1h-2023-10-27.zip
|
||||
"""
|
||||
url = (
|
||||
f"https://data.binance.vision/data/{asset_type_url_segment}/daily/klines/{symbol}"
|
||||
f"/{timeframe}/{binance_vision_zip_name(symbol, timeframe, date)}"
|
||||
)
|
||||
return url
|
||||
|
||||
|
||||
async def get_daily_ohlcv(
|
||||
asset_type_url_segment: str,
|
||||
symbol: str,
|
||||
timeframe: str,
|
||||
date: date,
|
||||
session: aiohttp.ClientSession,
|
||||
retry_count: int = 3,
|
||||
retry_delay: float = 0.0,
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Get daily OHLCV from https://data.binance.vision
|
||||
See https://github.com/binance/binance-public-data
|
||||
|
||||
:asset_type_url_segment: `spot` or `futures/um`
|
||||
:symbol: binance symbol name, e.g. BTCUSDT
|
||||
:timeframe: e.g. 1m, 1h
|
||||
:date: the returned DataFrame will cover the entire day of `date` in UTC
|
||||
:session: an aiohttp.ClientSession instance
|
||||
:retry_count: times to retry before returning the exceptions
|
||||
:retry_delay: the time to wait before every retry
|
||||
:return: A dataframe containing columns date,open,high,low,close,volume
|
||||
"""
|
||||
|
||||
url = binance_vision_zip_url(asset_type_url_segment, symbol, timeframe, date)
|
||||
|
||||
logger.debug(f"download data from binance: {url}")
|
||||
|
||||
retry = 0
|
||||
while True:
|
||||
if retry > 0:
|
||||
sleep_secs = retry * retry_delay
|
||||
logger.debug(
|
||||
f"[{retry}/{retry_count}] retry to download {url} after {sleep_secs} seconds"
|
||||
)
|
||||
await asyncio.sleep(sleep_secs)
|
||||
try:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
content = await resp.read()
|
||||
logger.debug(f"Successfully downloaded {url}")
|
||||
with zipfile.ZipFile(BytesIO(content)) as zipf:
|
||||
with zipf.open(zipf.namelist()[0]) as csvf:
|
||||
# https://github.com/binance/binance-public-data/issues/283
|
||||
first_byte = csvf.read(1)[0]
|
||||
if chr(first_byte).isdigit():
|
||||
header = None
|
||||
else:
|
||||
header = 0
|
||||
csvf.seek(0)
|
||||
|
||||
df = pd.read_csv(
|
||||
csvf,
|
||||
usecols=[0, 1, 2, 3, 4, 5],
|
||||
names=["date", "open", "high", "low", "close", "volume"],
|
||||
header=header,
|
||||
)
|
||||
df["date"] = pd.to_datetime(df["date"], unit="ms", utc=True)
|
||||
return df
|
||||
elif resp.status == 404:
|
||||
logger.debug(f"Failed to download {url}")
|
||||
raise Http404(f"404: {url}", date, url)
|
||||
else:
|
||||
raise BadHttpStatus(f"{resp.status} - {resp.reason}")
|
||||
except Exception as e:
|
||||
retry += 1
|
||||
if isinstance(e, Http404) or retry > retry_count:
|
||||
logger.debug(f"Failed to get data from {url}: {e}")
|
||||
raise
|
||||
@@ -7,7 +7,7 @@ import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
import signal
|
||||
from collections.abc import Coroutine
|
||||
from collections.abc import Coroutine, Generator
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import floor, isnan
|
||||
@@ -201,7 +201,7 @@ class Exchange:
|
||||
|
||||
self._cache_lock = Lock()
|
||||
# Cache for 10 minutes ...
|
||||
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
|
||||
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=4, ttl=60 * 10)
|
||||
# Cache values for 300 to avoid frequent polling of the exchange for prices
|
||||
# Caching only applies to RPC methods, so prices for open trades are still
|
||||
# refreshed once every iteration.
|
||||
@@ -705,14 +705,22 @@ class Exchange:
|
||||
f"Available currencies are: {', '.join(quote_currencies)}"
|
||||
)
|
||||
|
||||
def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
|
||||
def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> Generator[str, None, None]:
|
||||
"""
|
||||
Get valid pair combination of curr_1 and curr_2 by trying both combinations.
|
||||
"""
|
||||
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
|
||||
yielded = False
|
||||
for pair in (
|
||||
f"{curr_1}/{curr_2}",
|
||||
f"{curr_2}/{curr_1}",
|
||||
f"{curr_1}/{curr_2}:{curr_2}",
|
||||
f"{curr_2}/{curr_1}:{curr_1}",
|
||||
):
|
||||
if pair in self.markets and self.markets[pair].get("active"):
|
||||
return pair
|
||||
raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
||||
yielded = True
|
||||
yield pair
|
||||
if not yielded:
|
||||
raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
||||
|
||||
def validate_timeframes(self, timeframe: str | None) -> None:
|
||||
"""
|
||||
@@ -1801,24 +1809,37 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers:
|
||||
def get_tickers(
|
||||
self,
|
||||
symbols: list[str] | None = None,
|
||||
*,
|
||||
cached: bool = False,
|
||||
market_type: TradingMode | None = None,
|
||||
) -> Tickers:
|
||||
"""
|
||||
:param symbols: List of symbols to fetch
|
||||
:param cached: Allow cached result
|
||||
:param market_type: Market type to fetch - either spot or futures.
|
||||
:return: fetch_tickers result
|
||||
"""
|
||||
tickers: Tickers
|
||||
if not self.exchange_has("fetchTickers"):
|
||||
return {}
|
||||
cache_key = f"fetch_tickers_{market_type}" if market_type else "fetch_tickers"
|
||||
if cached:
|
||||
with self._cache_lock:
|
||||
tickers = self._fetch_tickers_cache.get("fetch_tickers") # type: ignore
|
||||
tickers = self._fetch_tickers_cache.get(cache_key) # type: ignore
|
||||
if tickers:
|
||||
return tickers
|
||||
try:
|
||||
tickers = self._api.fetch_tickers(symbols)
|
||||
# Re-map futures to swap
|
||||
market_types = {
|
||||
TradingMode.FUTURES: "swap",
|
||||
}
|
||||
params = {"type": market_types.get(market_type, market_type)} if market_type else {}
|
||||
tickers = self._api.fetch_tickers(symbols, params)
|
||||
with self._cache_lock:
|
||||
self._fetch_tickers_cache["fetch_tickers"] = tickers
|
||||
self._fetch_tickers_cache[cache_key] = tickers
|
||||
return tickers
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
@@ -1842,7 +1863,39 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Pricing info
|
||||
def get_conversion_rate(self, coin: str, currency: str) -> float | None:
|
||||
"""
|
||||
Quick and cached way to get conversion rate one currency to the other.
|
||||
Can then be used as "rate * amount" to convert between currencies.
|
||||
:param coin: Coin to convert
|
||||
:param currency: Currency to convert to
|
||||
:returns: Conversion rate from coin to currency
|
||||
:raises: ExchangeErrors
|
||||
"""
|
||||
if coin == currency:
|
||||
return 1.0
|
||||
tickers = self.get_tickers(cached=True)
|
||||
try:
|
||||
for pair in self.get_valid_pair_combination(coin, currency):
|
||||
ticker: Ticker | None = tickers.get(pair, None)
|
||||
if not ticker:
|
||||
tickers_other: Tickers = self.get_tickers(
|
||||
cached=True,
|
||||
market_type=(
|
||||
TradingMode.SPOT
|
||||
if self.trading_mode != TradingMode.SPOT
|
||||
else TradingMode.FUTURES
|
||||
),
|
||||
)
|
||||
ticker = tickers_other.get(pair, None)
|
||||
if ticker:
|
||||
rate: float | None = ticker.get("last", None)
|
||||
if rate and pair.startswith(currency) and not pair.endswith(currency):
|
||||
rate = 1.0 / rate
|
||||
return rate
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
@retrier
|
||||
def fetch_ticker(self, pair: str) -> Ticker:
|
||||
@@ -2198,10 +2251,13 @@ class Exchange:
|
||||
# If cost is None or 0.0 -> falsy, return None
|
||||
return None
|
||||
try:
|
||||
comb = self.get_valid_pair_combination(fee_curr, self._config["stake_currency"])
|
||||
tick = self.fetch_ticker(comb)
|
||||
|
||||
fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask")
|
||||
for comb in self.get_valid_pair_combination(
|
||||
fee_curr, self._config["stake_currency"]
|
||||
):
|
||||
tick = self.fetch_ticker(comb)
|
||||
fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask")
|
||||
if tick:
|
||||
break
|
||||
except (ValueError, ExchangeError):
|
||||
fee_to_quote_rate = self._config["exchange"].get("unknown_fee_rate", None)
|
||||
if not fee_to_quote_rate:
|
||||
@@ -2244,8 +2300,9 @@ class Exchange:
|
||||
:param pair: Pair to download
|
||||
:param timeframe: Timeframe to get data for
|
||||
:param since_ms: Timestamp in milliseconds to get history from
|
||||
:param until_ms: Timestamp in milliseconds to get history up to
|
||||
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
||||
:param until_ms: Timestamp in milliseconds to get history up to
|
||||
:return: Dataframe with candle (OHLCV) data
|
||||
"""
|
||||
pair, _, _, data, _ = self.loop.run_until_complete(
|
||||
@@ -2254,11 +2311,10 @@ class Exchange:
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms,
|
||||
until_ms=until_ms,
|
||||
is_new_pair=is_new_pair,
|
||||
candle_type=candle_type,
|
||||
)
|
||||
)
|
||||
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||
logger.debug(f"Downloaded data for {pair} from ccxt with length {len(data)}.")
|
||||
return ohlcv_to_dataframe(data, timeframe, pair, fill_missing=False, drop_incomplete=True)
|
||||
|
||||
async def _async_get_historic_ohlcv(
|
||||
@@ -2267,13 +2323,11 @@ class Exchange:
|
||||
timeframe: str,
|
||||
since_ms: int,
|
||||
candle_type: CandleType,
|
||||
is_new_pair: bool = False,
|
||||
raise_: bool = False,
|
||||
until_ms: int | None = None,
|
||||
) -> OHLCVResponse:
|
||||
"""
|
||||
Download historic ohlcv
|
||||
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
"""
|
||||
|
||||
|
||||
@@ -308,7 +308,9 @@ def price_to_precision(
|
||||
decimal_to_precision(
|
||||
price,
|
||||
rounding_mode=rounding_mode,
|
||||
precision=price_precision,
|
||||
precision=int(price_precision)
|
||||
if precisionMode != TICK_SIZE
|
||||
else price_precision,
|
||||
counting_mode=precisionMode,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -50,11 +50,17 @@ class Kraken(Exchange):
|
||||
|
||||
return parent_check and market.get("darkpool", False) is False
|
||||
|
||||
def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers:
|
||||
def get_tickers(
|
||||
self,
|
||||
symbols: list[str] | None = None,
|
||||
*,
|
||||
cached: bool = False,
|
||||
market_type: TradingMode | None = None,
|
||||
) -> Tickers:
|
||||
# Only fetch tickers for current stake currency
|
||||
# Otherwise the request for kraken becomes too large.
|
||||
symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]]))
|
||||
return super().get_tickers(symbols=symbols, cached=cached)
|
||||
return super().get_tickers(symbols=symbols, cached=cached, market_type=market_type)
|
||||
|
||||
def consolidate_balances(self, balances: CcxtBalances) -> CcxtBalances:
|
||||
"""
|
||||
|
||||
@@ -849,7 +849,7 @@ class FreqaiDataKitchen:
|
||||
dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata)
|
||||
dataframe = self.remove_special_chars_from_feature_names(dataframe)
|
||||
|
||||
self.get_unique_classes_from_labels(dataframe)
|
||||
self.get_unique_classes_from_labels(dataframe)
|
||||
|
||||
if self.config.get("reduce_df_footprint", False):
|
||||
dataframe = reduce_dataframe_footprint(dataframe)
|
||||
|
||||
@@ -5,7 +5,6 @@ from xgboost import XGBRFRegressor
|
||||
|
||||
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.tensorboard import TBCallback
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -45,7 +44,12 @@ class XGBoostRFRegressor(BaseRegressionModel):
|
||||
|
||||
model = XGBRFRegressor(**self.model_training_parameters)
|
||||
|
||||
model.set_params(callbacks=[TBCallback(dk.data_path)])
|
||||
# Callbacks are not supported for XGBRFRegressor, and version 2.1.x started to throw
|
||||
# the following error:
|
||||
# NotImplementedError: `early_stopping_rounds` and `callbacks` are not implemented
|
||||
# for random forest.
|
||||
|
||||
# model.set_params(callbacks=[TBCallback(dk.data_path)])
|
||||
model.fit(
|
||||
X=X,
|
||||
y=y,
|
||||
@@ -55,6 +59,6 @@ class XGBoostRFRegressor(BaseRegressionModel):
|
||||
xgb_model=xgb_model,
|
||||
)
|
||||
# set the callbacks to empty so that we can serialize to disk later
|
||||
model.set_params(callbacks=[])
|
||||
# model.set_params(callbacks=[])
|
||||
|
||||
return model
|
||||
|
||||
@@ -27,10 +27,13 @@ def update_liquidation_prices(
|
||||
total_wallet_stake = 0.0
|
||||
if dry_run:
|
||||
# Parameters only needed for cross margin
|
||||
total_wallet_stake = wallets.get_total(stake_currency)
|
||||
total_wallet_stake = wallets.get_collateral()
|
||||
|
||||
logger.info("Updating liquidation price for all open trades.")
|
||||
open_trades = Trade.get_open_trades()
|
||||
logger.info(
|
||||
"Updating liquidation price for all open trades. "
|
||||
f"Collateral {total_wallet_stake} {stake_currency}."
|
||||
)
|
||||
open_trades: list[Trade] = Trade.get_open_trades()
|
||||
for t in open_trades:
|
||||
# TODO: This should be done in a batch update
|
||||
t.set_liquidation_price(
|
||||
|
||||
@@ -10,7 +10,7 @@ from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.optimize.analysis.lookahead import LookaheadAnalysis
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.util import print_rich_table
|
||||
from freqtrade.util import get_dry_run_wallet, print_rich_table
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -163,7 +163,7 @@ class LookaheadAnalysisSubFunctions:
|
||||
config["max_open_trades"] = len(config["pairs"])
|
||||
|
||||
min_dry_run_wallet = 1000000000
|
||||
if config["dry_run_wallet"] < min_dry_run_wallet:
|
||||
if get_dry_run_wallet(config) < min_dry_run_wallet:
|
||||
logger.info(
|
||||
"Dry run wallet was not set to 1 billion, pushing it up there "
|
||||
"just to avoid false positives"
|
||||
|
||||
@@ -47,8 +47,7 @@ from freqtrade.optimize.optimize_reports import (
|
||||
generate_rejected_signals,
|
||||
generate_trade_signal_candles,
|
||||
show_backtest_results,
|
||||
store_backtest_analysis_results,
|
||||
store_backtest_stats,
|
||||
store_backtest_results,
|
||||
)
|
||||
from freqtrade.persistence import (
|
||||
CustomDataWrapper,
|
||||
@@ -121,10 +120,12 @@ class Backtesting:
|
||||
self.run_ids: dict[str, str] = {}
|
||||
self.strategylist: list[IStrategy] = []
|
||||
self.all_results: dict[str, dict] = {}
|
||||
self.processed_dfs: dict[str, dict] = {}
|
||||
self.analysis_results: dict[str, dict[str, DataFrame]] = {
|
||||
"signals": {},
|
||||
"rejected": {},
|
||||
"exited": {},
|
||||
}
|
||||
self.rejected_dict: dict[str, list] = {}
|
||||
self.rejected_df: dict[str, dict] = {}
|
||||
self.exited_dfs: dict[str, dict] = {}
|
||||
|
||||
self._exchange_name = self.config["exchange"]["name"]
|
||||
if not exchange:
|
||||
@@ -1629,15 +1630,13 @@ class Backtesting:
|
||||
self.config.get("export", "none") == "signals"
|
||||
and self.dataprovider.runmode == RunMode.BACKTEST
|
||||
):
|
||||
self.processed_dfs[strategy_name] = generate_trade_signal_candles(
|
||||
preprocessed_tmp, results, "open_date"
|
||||
)
|
||||
self.rejected_df[strategy_name] = generate_rejected_signals(
|
||||
preprocessed_tmp, self.rejected_dict
|
||||
)
|
||||
self.exited_dfs[strategy_name] = generate_trade_signal_candles(
|
||||
preprocessed_tmp, results, "close_date"
|
||||
)
|
||||
signals = generate_trade_signal_candles(preprocessed_tmp, results, "open_date")
|
||||
rejected = generate_rejected_signals(preprocessed_tmp, self.rejected_dict)
|
||||
exited = generate_trade_signal_candles(preprocessed_tmp, results, "close_date")
|
||||
|
||||
self.analysis_results["signals"][strategy_name] = signals
|
||||
self.analysis_results["rejected"][strategy_name] = rejected
|
||||
self.analysis_results["exited"][strategy_name] = exited
|
||||
|
||||
return min_date, max_date
|
||||
|
||||
@@ -1701,23 +1700,12 @@ class Backtesting:
|
||||
dt_appendix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
if self.config.get("export", "none") in ("trades", "signals"):
|
||||
combined_res = combined_dataframes_with_rel_mean(data, min_date, max_date)
|
||||
store_backtest_stats(
|
||||
self.config["exportfilename"],
|
||||
store_backtest_results(
|
||||
self.config,
|
||||
self.results,
|
||||
dt_appendix,
|
||||
market_change_data=combined_res,
|
||||
)
|
||||
|
||||
if (
|
||||
self.config.get("export", "none") == "signals"
|
||||
and self.dataprovider.runmode == RunMode.BACKTEST
|
||||
):
|
||||
store_backtest_analysis_results(
|
||||
self.config["exportfilename"],
|
||||
self.processed_dfs,
|
||||
self.rejected_df,
|
||||
self.exited_dfs,
|
||||
dt_appendix,
|
||||
analysis_results=self.analysis_results,
|
||||
)
|
||||
|
||||
# Results may be mixed up now. Sort them so they follow --strategy-list order.
|
||||
|
||||
@@ -28,6 +28,7 @@ from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLo
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer, HyperoptTools
|
||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
|
||||
|
||||
|
||||
# Suppress scikit-learn FutureWarnings from skopt
|
||||
@@ -363,6 +364,7 @@ class HyperOptimizer:
|
||||
config=self.config,
|
||||
processed=processed,
|
||||
backtest_stats=strat_stats,
|
||||
starting_balance=get_dry_run_wallet(self.config),
|
||||
)
|
||||
return {
|
||||
"loss": loss,
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_calmar
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -24,10 +23,9 @@ class CalmarHyperOptLoss(IHyperOptLoss):
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Config,
|
||||
starting_balance: float,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
@@ -36,7 +34,6 @@ class CalmarHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
Uses Calmar Ratio calculation.
|
||||
"""
|
||||
starting_balance = config["dry_run_wallet"]
|
||||
calmar_ratio = calculate_calmar(results, min_date, max_date, starting_balance)
|
||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||
return -calmar_ratio
|
||||
|
||||
@@ -31,6 +31,7 @@ class IHyperOptLoss(ABC):
|
||||
config: Config,
|
||||
processed: dict[str, DataFrame],
|
||||
backtest_stats: dict[str, Any],
|
||||
starting_balance: float,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
"""
|
||||
|
||||
@@ -7,7 +7,6 @@ Hyperoptimization.
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_underwater
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -21,7 +20,9 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, config: Config, *args, **kwargs) -> float:
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame, starting_balance: float, *args, **kwargs
|
||||
) -> float:
|
||||
"""
|
||||
Objective function.
|
||||
|
||||
@@ -31,7 +32,7 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss):
|
||||
total_profit = results["profit_abs"].sum()
|
||||
try:
|
||||
drawdown_df = calculate_underwater(
|
||||
results, value_col="profit_abs", starting_balance=config["dry_run_wallet"]
|
||||
results, value_col="profit_abs", starting_balance=starting_balance
|
||||
)
|
||||
max_drawdown = abs(min(drawdown_df["drawdown"]))
|
||||
relative_drawdown = max(drawdown_df["drawdown_relative"])
|
||||
|
||||
@@ -33,7 +33,6 @@ TARGET_TRADE_AMOUNT variable sets the minimum number of trades required to avoid
|
||||
import numpy as np
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -57,7 +56,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss):
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
config: Config,
|
||||
starting_balance: float,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
total_profit = results["profit_abs"].sum()
|
||||
@@ -83,7 +82,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss):
|
||||
# Calculate drawdown
|
||||
try:
|
||||
drawdown = calculate_max_drawdown(
|
||||
results, starting_balance=config["dry_run_wallet"], value_col="profit_abs"
|
||||
results, starting_balance=starting_balance, value_col="profit_abs"
|
||||
)
|
||||
relative_account_drawdown = drawdown.relative_account_drawdown
|
||||
except ValueError:
|
||||
|
||||
@@ -10,7 +10,6 @@ individual needs.
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_max_drawdown
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -21,12 +20,14 @@ DRAWDOWN_MULT = 0.075
|
||||
|
||||
class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, config: Config, *args, **kwargs) -> float:
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame, starting_balance: float, *args, **kwargs
|
||||
) -> float:
|
||||
total_profit = results["profit_abs"].sum()
|
||||
|
||||
try:
|
||||
drawdown = calculate_max_drawdown(
|
||||
results, starting_balance=config["dry_run_wallet"], value_col="profit_abs"
|
||||
results, starting_balance=starting_balance, value_col="profit_abs"
|
||||
)
|
||||
relative_account_drawdown = drawdown.relative_account_drawdown
|
||||
except ValueError:
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_sharpe
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -24,10 +23,9 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Config,
|
||||
starting_balance: float,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
@@ -36,7 +34,6 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
Uses Sharpe Ratio calculation.
|
||||
"""
|
||||
starting_balance = config["dry_run_wallet"]
|
||||
sharp_ratio = calculate_sharpe(results, min_date, max_date, starting_balance)
|
||||
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||
return -sharp_ratio
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_sortino
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -24,10 +23,9 @@ class SortinoHyperOptLoss(IHyperOptLoss):
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Config,
|
||||
starting_balance: float,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
@@ -36,7 +34,6 @@ class SortinoHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
Uses Sortino Ratio calculation.
|
||||
"""
|
||||
starting_balance = config["dry_run_wallet"]
|
||||
sortino_ratio = calculate_sortino(results, min_date, max_date, starting_balance)
|
||||
# print(expected_returns_mean, down_stdev, sortino_ratio)
|
||||
return -sortino_ratio
|
||||
|
||||
@@ -11,10 +11,7 @@ from freqtrade.optimize.optimize_reports.bt_output import (
|
||||
text_table_strategy,
|
||||
text_table_tags,
|
||||
)
|
||||
from freqtrade.optimize.optimize_reports.bt_storage import (
|
||||
store_backtest_analysis_results,
|
||||
store_backtest_stats,
|
||||
)
|
||||
from freqtrade.optimize.optimize_reports.bt_storage import store_backtest_results
|
||||
from freqtrade.optimize.optimize_reports.optimize_reports import (
|
||||
generate_all_periodic_breakdown_stats,
|
||||
generate_backtest_stats,
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||
from freqtrade.enums.runmode import RunMode
|
||||
from freqtrade.ft_types import BacktestResultType
|
||||
from freqtrade.misc import file_dump_joblib, file_dump_json
|
||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||
@@ -29,21 +30,27 @@ def _generate_filename(recordfilename: Path, appendix: str, suffix: str) -> Path
|
||||
return filename
|
||||
|
||||
|
||||
def store_backtest_stats(
|
||||
recordfilename: Path,
|
||||
def store_backtest_results(
|
||||
config: dict,
|
||||
stats: BacktestResultType,
|
||||
dtappendix: str,
|
||||
*,
|
||||
market_change_data: DataFrame | None = None,
|
||||
analysis_results: dict[str, dict[str, DataFrame]] | None = None,
|
||||
) -> Path:
|
||||
"""
|
||||
Stores backtest results
|
||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||
Filenames will be appended with a timestamp right before the suffix
|
||||
while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
|
||||
Stores backtest results and analysis data
|
||||
:param config: Configuration dictionary
|
||||
:param stats: Dataframe containing the backtesting statistics
|
||||
:param dtappendix: Datetime to use for the filename
|
||||
:param market_change_data: Dataframe containing market change data
|
||||
:param analysis_results: Dictionary containing analysis results
|
||||
"""
|
||||
|
||||
# Path object, which can either be a filename or a directory.
|
||||
# Filenames will be appended with a timestamp right before the suffix
|
||||
# while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
|
||||
recordfilename: Path = config["exportfilename"]
|
||||
filename = _generate_filename(recordfilename, dtappendix, ".json")
|
||||
|
||||
# Store metadata separately.
|
||||
@@ -65,6 +72,21 @@ def store_backtest_stats(
|
||||
filename_mc, compression_level=9, compression="lz4"
|
||||
)
|
||||
|
||||
if (
|
||||
config.get("export", "none") == "signals"
|
||||
and analysis_results is not None
|
||||
and config.get("runmode", RunMode.OTHER) == RunMode.BACKTEST
|
||||
):
|
||||
_store_backtest_analysis_data(
|
||||
recordfilename, analysis_results["signals"], dtappendix, "signals"
|
||||
)
|
||||
_store_backtest_analysis_data(
|
||||
recordfilename, analysis_results["rejected"], dtappendix, "rejected"
|
||||
)
|
||||
_store_backtest_analysis_data(
|
||||
recordfilename, analysis_results["exited"], dtappendix, "exited"
|
||||
)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
@@ -86,15 +108,3 @@ def _store_backtest_analysis_data(
|
||||
file_dump_joblib(filename, data)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def store_backtest_analysis_results(
|
||||
recordfilename: Path,
|
||||
candles: dict[str, dict],
|
||||
trades: dict[str, dict],
|
||||
exited: dict[str, dict],
|
||||
dtappendix: str,
|
||||
) -> None:
|
||||
_store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals")
|
||||
_store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected")
|
||||
_store_backtest_analysis_data(recordfilename, exited, dtappendix, "exited")
|
||||
|
||||
@@ -18,7 +18,7 @@ from freqtrade.data.metrics import (
|
||||
calculate_sortino,
|
||||
)
|
||||
from freqtrade.ft_types import BacktestResultType
|
||||
from freqtrade.util import decimals_per_coin, fmt_coin
|
||||
from freqtrade.util import decimals_per_coin, fmt_coin, get_dry_run_wallet
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -69,7 +69,7 @@ def generate_rejected_signals(
|
||||
|
||||
|
||||
def _generate_result_line(
|
||||
result: DataFrame, starting_balance: int, first_column: str | list[str]
|
||||
result: DataFrame, starting_balance: float, first_column: str | list[str]
|
||||
) -> dict:
|
||||
"""
|
||||
Generate one result dict, with "first_column" as key.
|
||||
@@ -111,7 +111,7 @@ def _generate_result_line(
|
||||
def generate_pair_metrics(
|
||||
pairlist: list[str],
|
||||
stake_currency: str,
|
||||
starting_balance: int,
|
||||
starting_balance: float,
|
||||
results: DataFrame,
|
||||
skip_nan: bool = False,
|
||||
) -> list[dict]:
|
||||
@@ -144,7 +144,7 @@ def generate_pair_metrics(
|
||||
|
||||
def generate_tag_metrics(
|
||||
tag_type: Literal["enter_tag", "exit_reason"] | list[Literal["enter_tag", "exit_reason"]],
|
||||
starting_balance: int,
|
||||
starting_balance: float,
|
||||
results: DataFrame,
|
||||
skip_nan: bool = False,
|
||||
) -> list[dict]:
|
||||
@@ -373,7 +373,7 @@ def generate_strategy_stats(
|
||||
return {}
|
||||
config = content["config"]
|
||||
max_open_trades = min(config["max_open_trades"], len(pairlist))
|
||||
start_balance = config["dry_run_wallet"]
|
||||
start_balance = get_dry_run_wallet(config)
|
||||
stake_currency = config["stake_currency"]
|
||||
|
||||
pair_results = generate_pair_metrics(
|
||||
|
||||
@@ -39,7 +39,7 @@ class PairLock(ModelBase):
|
||||
|
||||
@staticmethod
|
||||
def query_pair_locks(
|
||||
pair: str | None, now: datetime, side: str = "*"
|
||||
pair: str | None, now: datetime, side: str | None = None
|
||||
) -> ScalarResult["PairLock"]:
|
||||
"""
|
||||
Get all currently active locks for this pair
|
||||
@@ -53,9 +53,9 @@ class PairLock(ModelBase):
|
||||
]
|
||||
if pair:
|
||||
filters.append(PairLock.pair == pair)
|
||||
if side != "*":
|
||||
if side is not None and side != "*":
|
||||
filters.append(or_(PairLock.side == side, PairLock.side == "*"))
|
||||
else:
|
||||
elif side is not None:
|
||||
filters.append(PairLock.side == "*")
|
||||
|
||||
return PairLock.session.scalars(select(PairLock).filter(*filters))
|
||||
|
||||
@@ -67,13 +67,14 @@ class PairLocks:
|
||||
|
||||
@staticmethod
|
||||
def get_pair_locks(
|
||||
pair: str | None, now: datetime | None = None, side: str = "*"
|
||||
pair: str | None, now: datetime | None = None, side: str | None = None
|
||||
) -> Sequence[PairLock]:
|
||||
"""
|
||||
Get all currently active locks for this pair
|
||||
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||
defaults to datetime.now(timezone.utc)
|
||||
:param side: Side get locks for, can be 'long', 'short', '*' or None
|
||||
"""
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -88,7 +89,7 @@ class PairLocks:
|
||||
lock.lock_end_time >= now
|
||||
and lock.active is True
|
||||
and (pair is None or lock.pair == pair)
|
||||
and (lock.side == "*" or lock.side == side)
|
||||
and (side is None or lock.side == "*" or lock.side == side)
|
||||
)
|
||||
]
|
||||
return locks
|
||||
|
||||
@@ -172,12 +172,20 @@ class Order(ModelBase):
|
||||
@property
|
||||
def stake_amount(self) -> float:
|
||||
"""Amount in stake currency used for this order"""
|
||||
return self.safe_amount * self.safe_price / self.trade.leverage
|
||||
return float(
|
||||
FtPrecise(self.safe_amount)
|
||||
* FtPrecise(self.safe_price)
|
||||
/ FtPrecise(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
|
||||
return float(
|
||||
FtPrecise(self.safe_filled)
|
||||
* FtPrecise(self.safe_price)
|
||||
/ FtPrecise(self.trade.leverage)
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
@@ -777,7 +785,9 @@ class LocalTrade:
|
||||
"""
|
||||
if liquidation_price is None:
|
||||
return
|
||||
self.liquidation_price = liquidation_price
|
||||
self.liquidation_price = price_to_precision(
|
||||
liquidation_price, self.price_precision, self.precision_mode_price
|
||||
)
|
||||
|
||||
def set_funding_fees(self, funding_fee: float) -> None:
|
||||
"""
|
||||
|
||||
@@ -28,6 +28,7 @@ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy import IStrategy
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.util import get_dry_run_wallet
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -706,7 +707,7 @@ def plot_profit(config: Config) -> None:
|
||||
trades,
|
||||
config["timeframe"],
|
||||
config.get("stake_currency", ""),
|
||||
config.get("available_capital", config["dry_run_wallet"]),
|
||||
config.get("available_capital", get_dry_run_wallet(config)),
|
||||
)
|
||||
store_plot_file(
|
||||
fig,
|
||||
|
||||
@@ -8,7 +8,7 @@ defined period or as coming from ticker
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from typing import TypedDict
|
||||
|
||||
from cachetools import TTLCache
|
||||
from pandas import DataFrame
|
||||
@@ -24,6 +24,11 @@ from freqtrade.util import dt_now, format_ms_time
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SymbolWithPercentage(TypedDict):
|
||||
symbol: str
|
||||
percentage: float | None
|
||||
|
||||
|
||||
class PercentChangePairList(IPairList):
|
||||
is_pairlist_generator = True
|
||||
supports_backtesting = SupportsBacktesting.NO
|
||||
@@ -191,7 +196,6 @@ class PercentChangePairList(IPairList):
|
||||
for k, v in tickers.items()
|
||||
if (
|
||||
self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||
and (self._use_range or v.get("percentage") is not None)
|
||||
and v["symbol"] in _pairlist
|
||||
)
|
||||
]
|
||||
@@ -212,13 +216,15 @@ class PercentChangePairList(IPairList):
|
||||
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
||||
:return: new whitelist
|
||||
"""
|
||||
filtered_tickers: list[dict[str, Any]] = [{"symbol": k} for k in pairlist]
|
||||
filtered_tickers: list[SymbolWithPercentage] = [
|
||||
{"symbol": k, "percentage": None} for k in pairlist
|
||||
]
|
||||
if self._use_range:
|
||||
# calculating using lookback_period
|
||||
self.fetch_percent_change_from_lookback_period(filtered_tickers)
|
||||
filtered_tickers = self.fetch_percent_change_from_lookback_period(filtered_tickers)
|
||||
else:
|
||||
# Fetching 24h change by default from supported exchange tickers
|
||||
self.fetch_percent_change_from_tickers(filtered_tickers, tickers)
|
||||
filtered_tickers = self.fetch_percent_change_from_tickers(filtered_tickers, tickers)
|
||||
|
||||
if self._min_value is not None:
|
||||
filtered_tickers = [v for v in filtered_tickers if v["percentage"] > self._min_value]
|
||||
@@ -228,7 +234,7 @@ class PercentChangePairList(IPairList):
|
||||
sorted_tickers = sorted(
|
||||
filtered_tickers,
|
||||
reverse=self._sort_direction == "desc",
|
||||
key=lambda t: t["percentage"],
|
||||
key=lambda t: t["percentage"], # type: ignore
|
||||
)
|
||||
|
||||
# Validate whitelist to only have active market pairs
|
||||
@@ -240,7 +246,7 @@ class PercentChangePairList(IPairList):
|
||||
return pairs
|
||||
|
||||
def fetch_candles_for_lookback_period(
|
||||
self, filtered_tickers: list[dict[str, str]]
|
||||
self, filtered_tickers: list[SymbolWithPercentage]
|
||||
) -> dict[PairWithTimeframe, DataFrame]:
|
||||
since_ms = (
|
||||
int(
|
||||
@@ -262,7 +268,6 @@ class PercentChangePairList(IPairList):
|
||||
)
|
||||
* 1000
|
||||
)
|
||||
# todo: utc date output for starting date
|
||||
self.log_once(
|
||||
f"Using change range of {self._lookback_period} candles, timeframe: "
|
||||
f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} "
|
||||
@@ -277,7 +282,9 @@ class PercentChangePairList(IPairList):
|
||||
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms)
|
||||
return candles
|
||||
|
||||
def fetch_percent_change_from_lookback_period(self, filtered_tickers: list[dict[str, Any]]):
|
||||
def fetch_percent_change_from_lookback_period(
|
||||
self, filtered_tickers: list[SymbolWithPercentage]
|
||||
) -> list[SymbolWithPercentage]:
|
||||
# get lookback period in ms, for exchange ohlcv fetch
|
||||
candles = self.fetch_candles_for_lookback_period(filtered_tickers)
|
||||
|
||||
@@ -302,16 +309,23 @@ class PercentChangePairList(IPairList):
|
||||
filtered_tickers[i]["percentage"] = pct_change
|
||||
else:
|
||||
filtered_tickers[i]["percentage"] = 0
|
||||
return filtered_tickers
|
||||
|
||||
def fetch_percent_change_from_tickers(self, filtered_tickers: list[dict[str, Any]], tickers):
|
||||
for i, p in enumerate(filtered_tickers):
|
||||
def fetch_percent_change_from_tickers(
|
||||
self, filtered_tickers: list[SymbolWithPercentage], tickers
|
||||
) -> list[SymbolWithPercentage]:
|
||||
valid_tickers: list[SymbolWithPercentage] = []
|
||||
for p in filtered_tickers:
|
||||
# Filter out assets
|
||||
if not self._validate_pair(
|
||||
p["symbol"], tickers[p["symbol"]] if p["symbol"] in tickers else None
|
||||
if (
|
||||
self._validate_pair(
|
||||
p["symbol"], tickers[p["symbol"]] if p["symbol"] in tickers else None
|
||||
)
|
||||
and p["symbol"] != "UNI/USDT"
|
||||
):
|
||||
filtered_tickers.remove(p)
|
||||
else:
|
||||
filtered_tickers[i]["percentage"] = tickers[p["symbol"]]["percentage"]
|
||||
p["percentage"] = tickers[p["symbol"]]["percentage"]
|
||||
valid_tickers.append(p)
|
||||
return valid_tickers
|
||||
|
||||
def _validate_pair(self, pair: str, ticker: Ticker | None) -> bool:
|
||||
"""
|
||||
|
||||
@@ -43,7 +43,7 @@ router = APIRouter()
|
||||
|
||||
def __run_backtest_bg(btconfig: Config):
|
||||
from freqtrade.data.metrics import combined_dataframes_with_rel_mean
|
||||
from freqtrade.optimize.optimize_reports import generate_backtest_stats, store_backtest_stats
|
||||
from freqtrade.optimize.optimize_reports import generate_backtest_stats, store_backtest_results
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
@@ -101,8 +101,8 @@ def __run_backtest_bg(btconfig: Config):
|
||||
|
||||
if btconfig.get("export", "none") == "trades":
|
||||
combined_res = combined_dataframes_with_rel_mean(ApiBG.bt["data"], min_date, max_date)
|
||||
fn = store_backtest_stats(
|
||||
btconfig["exportfilename"],
|
||||
fn = store_backtest_results(
|
||||
btconfig,
|
||||
ApiBG.bt["bt"].results,
|
||||
datetime.now().strftime("%Y-%m-%d_%H-%M-%S"),
|
||||
market_change_data=combined_res,
|
||||
|
||||
@@ -57,7 +57,7 @@ def pairlists_evaluate(
|
||||
config_loc = deepcopy(config)
|
||||
config_loc["stake_currency"] = ""
|
||||
config_loc["pairs"] = payload.pairs
|
||||
config_loc["timeframe"] = payload.timeframes
|
||||
config_loc["timerange"] = payload.timerange
|
||||
config_loc["days"] = payload.days
|
||||
config_loc["timeframes"] = payload.timeframes
|
||||
config_loc["erase"] = payload.erase
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi.exceptions import HTTPException
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.data.history import get_datahandler
|
||||
from freqtrade.enums import CandleType, TradingMode
|
||||
from freqtrade.enums import CandleType, State, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.api_server.api_schemas import (
|
||||
@@ -217,7 +217,7 @@ def edge(rpc: RPC = Depends(get_rpc)):
|
||||
|
||||
@router.get("/show_config", response_model=ShowConfig, tags=["info"])
|
||||
def show_config(rpc: RPC | None = Depends(get_rpc_optional), config=Depends(get_config)):
|
||||
state = ""
|
||||
state: State | str = ""
|
||||
strategy_version = None
|
||||
if rpc:
|
||||
state = rpc._freqtrade.state
|
||||
@@ -357,6 +357,7 @@ def pair_history(
|
||||
config = deepcopy(config)
|
||||
config.update(
|
||||
{
|
||||
"timeframe": timeframe,
|
||||
"strategy": strategy,
|
||||
"timerange": timerange,
|
||||
"freqaimodel": freqaimodel if freqaimodel else config.get("freqaimodel"),
|
||||
@@ -377,6 +378,7 @@ def pair_history_filtered(
|
||||
config = deepcopy(config)
|
||||
config.update(
|
||||
{
|
||||
"timeframe": payload.timeframe,
|
||||
"strategy": payload.strategy,
|
||||
"timerange": payload.timerange,
|
||||
"freqaimodel": (
|
||||
|
||||
+32
-24
@@ -7,7 +7,7 @@ from abc import abstractmethod
|
||||
from collections.abc import Generator, Sequence
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from math import isnan
|
||||
from typing import Any, cast
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import psutil
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@@ -32,7 +32,7 @@ from freqtrade.enums import (
|
||||
)
|
||||
from freqtrade.exceptions import ExchangeError, PricingError
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||
from freqtrade.exchange.exchange_types import Ticker, Tickers
|
||||
from freqtrade.exchange.exchange_utils import price_to_precision
|
||||
from freqtrade.loggers import bufferHandler
|
||||
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
|
||||
from freqtrade.persistence.models import PairLock
|
||||
@@ -98,6 +98,10 @@ class RPC:
|
||||
|
||||
# Bind _fiat_converter if needed
|
||||
_fiat_converter: CryptoToFiatConverter | None = None
|
||||
if TYPE_CHECKING:
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
|
||||
_freqtrade: FreqtradeBot
|
||||
|
||||
def __init__(self, freqtrade) -> None:
|
||||
"""
|
||||
@@ -201,7 +205,7 @@ class RPC:
|
||||
# calculate profit and send message to user
|
||||
if trade.is_open:
|
||||
try:
|
||||
current_rate = self._freqtrade.exchange.get_rate(
|
||||
current_rate: float = self._freqtrade.exchange.get_rate(
|
||||
trade.pair, side="exit", is_short=trade.is_short, refresh=False
|
||||
)
|
||||
except (ExchangeError, PricingError):
|
||||
@@ -219,7 +223,7 @@ class RPC:
|
||||
|
||||
else:
|
||||
# Closed trade ...
|
||||
current_rate = trade.close_rate
|
||||
current_rate = trade.close_rate or 0.0
|
||||
current_profit = trade.close_profit or 0.0
|
||||
current_profit_abs = trade.close_profit_abs or 0.0
|
||||
|
||||
@@ -243,7 +247,11 @@ class RPC:
|
||||
stoploss_entry_dist_ratio = stop_entry.profit_ratio
|
||||
|
||||
# calculate distance to stoploss
|
||||
stoploss_current_dist = trade.stop_loss - current_rate
|
||||
stoploss_current_dist = price_to_precision(
|
||||
trade.stop_loss - current_rate,
|
||||
trade.price_precision,
|
||||
trade.precision_mode_price,
|
||||
)
|
||||
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
|
||||
|
||||
trade_dict = trade.to_json()
|
||||
@@ -572,8 +580,8 @@ class RPC:
|
||||
# Doing the sum is not right - overall profit needs to be based on initial capital
|
||||
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
|
||||
starting_balance = self._freqtrade.wallets.get_starting_balance()
|
||||
profit_closed_ratio_fromstart = 0
|
||||
profit_all_ratio_fromstart = 0
|
||||
profit_closed_ratio_fromstart = 0.0
|
||||
profit_all_ratio_fromstart = 0.0
|
||||
if starting_balance:
|
||||
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
|
||||
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
|
||||
@@ -670,7 +678,7 @@ class RPC:
|
||||
}
|
||||
|
||||
def __balance_get_est_stake(
|
||||
self, coin: str, stake_currency: str, amount: float, balance: Wallet, tickers: Tickers
|
||||
self, coin: str, stake_currency: str, amount: float, balance: Wallet
|
||||
) -> tuple[float, float]:
|
||||
est_stake = 0.0
|
||||
est_bot_stake = 0.0
|
||||
@@ -681,14 +689,18 @@ class RPC:
|
||||
est_stake = balance.free
|
||||
est_bot_stake = amount
|
||||
else:
|
||||
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
||||
rate: float | None = cast(Ticker, tickers.get(pair, {})).get("last", None)
|
||||
if rate:
|
||||
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
||||
rate = 1.0 / rate
|
||||
est_stake = rate * balance.total
|
||||
est_bot_stake = rate * amount
|
||||
try:
|
||||
rate: float | None = self._freqtrade.exchange.get_conversion_rate(
|
||||
coin, stake_currency
|
||||
)
|
||||
if rate:
|
||||
est_stake = rate * balance.total
|
||||
est_bot_stake = rate * amount
|
||||
|
||||
return est_stake, est_bot_stake
|
||||
except (ExchangeError, PricingError) as e:
|
||||
logger.warning(f"Error {e} getting rate for {coin}")
|
||||
pass
|
||||
return est_stake, est_bot_stake
|
||||
|
||||
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> dict:
|
||||
@@ -696,10 +708,6 @@ class RPC:
|
||||
currencies: list[dict] = []
|
||||
total = 0.0
|
||||
total_bot = 0.0
|
||||
try:
|
||||
tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True)
|
||||
except ExchangeError:
|
||||
raise RPCException("Error getting current tickers.")
|
||||
|
||||
open_trades: list[Trade] = Trade.get_open_trades()
|
||||
open_assets: dict[str, Trade] = {t.safe_base_currency: t for t in open_trades}
|
||||
@@ -715,7 +723,7 @@ class RPC:
|
||||
coin: str
|
||||
balance: Wallet
|
||||
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
||||
if not balance.total:
|
||||
if not balance.total and not balance.free:
|
||||
continue
|
||||
|
||||
trade = open_assets.get(coin, None)
|
||||
@@ -726,7 +734,7 @@ class RPC:
|
||||
|
||||
try:
|
||||
est_stake, est_stake_bot = self.__balance_get_est_stake(
|
||||
coin, stake_currency, trade_amount, balance, tickers
|
||||
coin, stake_currency, trade_amount, balance
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
@@ -886,10 +894,10 @@ class RPC:
|
||||
if amount and amount < trade.amount:
|
||||
# Partial exit ...
|
||||
min_exit_stake = self._freqtrade.exchange.get_min_pair_stake_amount(
|
||||
trade.pair, current_rate, trade.stop_loss_pct
|
||||
trade.pair, current_rate, trade.stop_loss_pct or 0.0
|
||||
)
|
||||
remaining = (trade.amount - amount) * current_rate
|
||||
if remaining < min_exit_stake:
|
||||
if min_exit_stake and remaining < min_exit_stake:
|
||||
raise RPCException(f"Remaining amount of {remaining} would be too small.")
|
||||
sub_amount = amount
|
||||
|
||||
@@ -1229,7 +1237,7 @@ class RPC:
|
||||
for pair in add:
|
||||
if pair not in self._freqtrade.pairlists.blacklist:
|
||||
try:
|
||||
expand_pairlist([pair], self._freqtrade.exchange.get_markets().keys())
|
||||
expand_pairlist([pair], list(self._freqtrade.exchange.get_markets().keys()))
|
||||
self._freqtrade.pairlists.blacklist.append(pair)
|
||||
|
||||
except ValueError:
|
||||
|
||||
@@ -5,7 +5,6 @@ This module defines the interface to apply for strategies
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import isinf, isnan
|
||||
|
||||
@@ -141,9 +140,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
market_direction: MarketDirection = MarketDirection.NONE
|
||||
|
||||
# Global cache dictionary
|
||||
_cached_grouped_trades_per_pair: dict[
|
||||
str, OrderedDict[tuple[datetime, datetime], DataFrame]
|
||||
] = {}
|
||||
_cached_grouped_trades_per_pair: dict[str, DataFrame] = {}
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
self.config = config
|
||||
@@ -1163,10 +1160,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
logger.warning(f"Empty candle (OHLCV) data for pair {pair}")
|
||||
return None, None
|
||||
|
||||
latest_date = dataframe["date"].max()
|
||||
latest = dataframe.loc[dataframe["date"] == latest_date].iloc[-1]
|
||||
latest_date_pd = dataframe["date"].max()
|
||||
latest = dataframe.loc[dataframe["date"] == latest_date_pd].iloc[-1]
|
||||
# Explicitly convert to datetime object to ensure the below comparison does not fail
|
||||
latest_date = latest_date.to_pydatetime()
|
||||
latest_date: datetime = latest_date_pd.to_pydatetime()
|
||||
|
||||
# Check if dataframe is out of date
|
||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||
@@ -1604,15 +1601,11 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
if use_public_trades:
|
||||
trades = self.dp.trades(pair=metadata["pair"], copy=False)
|
||||
|
||||
config = self.config
|
||||
config["timeframe"] = self.timeframe
|
||||
pair = metadata["pair"]
|
||||
# TODO: slice trades to size of dataframe for faster backtesting
|
||||
cached_grouped_trades: OrderedDict[tuple[datetime, datetime], DataFrame] = (
|
||||
self._cached_grouped_trades_per_pair.get(pair, OrderedDict())
|
||||
)
|
||||
cached_grouped_trades: DataFrame | None = self._cached_grouped_trades_per_pair.get(pair)
|
||||
dataframe, cached_grouped_trades = populate_dataframe_with_trades(
|
||||
cached_grouped_trades, config, dataframe, trades
|
||||
cached_grouped_trades, self.config, dataframe, trades
|
||||
)
|
||||
|
||||
# dereference old cache
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
"# Get market change (average change of all pairs from start to end of the backtest period)\n",
|
||||
"print(stats[\"strategy\"][strategy][\"market_change\"])\n",
|
||||
"# Maximum drawdown ()\n",
|
||||
"print(stats[\"strategy\"][strategy][\"max_drawdown\"])\n",
|
||||
"print(stats[\"strategy\"][strategy][\"max_drawdown_abs\"])\n",
|
||||
"# Maximum drawdown start and end\n",
|
||||
"print(stats[\"strategy\"][strategy][\"drawdown_start\"])\n",
|
||||
"print(stats[\"strategy\"][strategy][\"drawdown_end\"])\n",
|
||||
|
||||
@@ -11,6 +11,7 @@ from freqtrade.util.datetime_helpers import (
|
||||
format_ms_time,
|
||||
shorten_date,
|
||||
)
|
||||
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
|
||||
from freqtrade.util.formatters import decimals_per_coin, fmt_coin, fmt_coin2, round_value
|
||||
from freqtrade.util.ft_precise import FtPrecise
|
||||
from freqtrade.util.measure_time import MeasureTime
|
||||
@@ -35,6 +36,7 @@ __all__ = [
|
||||
"dt_utc",
|
||||
"format_date",
|
||||
"format_ms_time",
|
||||
"get_dry_run_wallet",
|
||||
"FtPrecise",
|
||||
"PeriodicCache",
|
||||
"shorten_date",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from freqtrade.constants import Config
|
||||
|
||||
|
||||
def get_dry_run_wallet(config: Config) -> int | float:
|
||||
"""
|
||||
Return dry-run wallet balance in stake currency from configuration.
|
||||
This setup also supports dictionary mode for dry-run-wallet.
|
||||
"""
|
||||
if isinstance(_start_cap := config["dry_run_wallet"], float | int):
|
||||
return _start_cap
|
||||
else:
|
||||
return _start_cap.get("stake_currency")
|
||||
+68
-35
@@ -2,7 +2,6 @@
|
||||
"""Wallet"""
|
||||
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta
|
||||
from typing import NamedTuple
|
||||
|
||||
@@ -41,7 +40,14 @@ class Wallets:
|
||||
self._exchange = exchange
|
||||
self._wallets: dict[str, Wallet] = {}
|
||||
self._positions: dict[str, PositionWallet] = {}
|
||||
self._start_cap = config["dry_run_wallet"]
|
||||
self._start_cap: dict[str, float] = {}
|
||||
self._stake_currency = config["stake_currency"]
|
||||
|
||||
if isinstance(_start_cap := config["dry_run_wallet"], float | int):
|
||||
self._start_cap[self._stake_currency] = _start_cap
|
||||
else:
|
||||
self._start_cap = _start_cap
|
||||
|
||||
self._last_wallet_refresh: datetime | None = None
|
||||
self.update()
|
||||
|
||||
@@ -66,6 +72,18 @@ class Wallets:
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_collateral(self) -> float:
|
||||
"""
|
||||
Get total collateral for liquidation price calculation.
|
||||
"""
|
||||
if self._config.get("margin_mode") == "cross":
|
||||
# free includes all balances and, combined with position collateral,
|
||||
# is used as "wallet balance".
|
||||
return self.get_free(self._stake_currency) + sum(
|
||||
pos.collateral for pos in self._positions.values()
|
||||
)
|
||||
return self.get_total(self._stake_currency)
|
||||
|
||||
def get_owned(self, pair: str, base_currency: str) -> float:
|
||||
"""
|
||||
Get currently owned value.
|
||||
@@ -109,54 +127,66 @@ class Wallets:
|
||||
for o in trade.open_orders
|
||||
if o.amount and o.ft_order_side == trade.exit_side
|
||||
)
|
||||
curr_wallet_bal = self._start_cap.get(curr, 0)
|
||||
|
||||
_wallets[curr] = Wallet(curr, trade.amount - pending, pending, trade.amount)
|
||||
|
||||
current_stake = self._start_cap + tot_profit - tot_in_trades
|
||||
total_stake = current_stake + used_stake
|
||||
_wallets[curr] = Wallet(
|
||||
curr,
|
||||
curr_wallet_bal + trade.amount - pending,
|
||||
pending,
|
||||
trade.amount + curr_wallet_bal,
|
||||
)
|
||||
else:
|
||||
tot_in_trades = 0
|
||||
for position in open_trades:
|
||||
# size = self._exchange._contracts_to_amount(position.pair, position['contracts'])
|
||||
size = position.amount
|
||||
collateral = position.stake_amount
|
||||
leverage = position.leverage
|
||||
tot_in_trades += collateral
|
||||
_positions[position.pair] = PositionWallet(
|
||||
position.pair,
|
||||
position=size,
|
||||
leverage=leverage,
|
||||
collateral=collateral,
|
||||
position=position.amount,
|
||||
leverage=position.leverage,
|
||||
collateral=position.stake_amount,
|
||||
side=position.trade_direction,
|
||||
)
|
||||
current_stake = self._start_cap + tot_profit - tot_in_trades
|
||||
used_stake = tot_in_trades
|
||||
total_stake = current_stake + tot_in_trades
|
||||
|
||||
_wallets[self._config["stake_currency"]] = Wallet(
|
||||
currency=self._config["stake_currency"],
|
||||
free=current_stake,
|
||||
used_stake = tot_in_trades
|
||||
|
||||
cross_margin = 0.0
|
||||
if self._config.get("margin_mode") == "cross":
|
||||
# In cross-margin mode, the total balance is used as collateral.
|
||||
# This is moved as "free" into the stake currency balance.
|
||||
# strongly tied to the get_collateral() implementation.
|
||||
for curr, bal in self._start_cap.items():
|
||||
if curr == self._stake_currency:
|
||||
continue
|
||||
rate = self._exchange.get_conversion_rate(curr, self._stake_currency)
|
||||
if rate:
|
||||
cross_margin += bal * rate
|
||||
|
||||
current_stake = self._start_cap.get(self._stake_currency, 0) + tot_profit - tot_in_trades
|
||||
total_stake = current_stake + used_stake
|
||||
|
||||
_wallets[self._stake_currency] = Wallet(
|
||||
currency=self._stake_currency,
|
||||
free=current_stake + cross_margin,
|
||||
used=used_stake,
|
||||
total=total_stake,
|
||||
)
|
||||
for currency, bal in self._start_cap.items():
|
||||
if currency not in _wallets:
|
||||
_wallets[currency] = Wallet(currency, bal, 0, bal)
|
||||
|
||||
self._wallets = _wallets
|
||||
self._positions = _positions
|
||||
|
||||
def _update_live(self) -> None:
|
||||
balances = self._exchange.get_balances()
|
||||
_wallets = {}
|
||||
|
||||
for currency in balances:
|
||||
if isinstance(balances[currency], dict):
|
||||
self._wallets[currency] = Wallet(
|
||||
_wallets[currency] = Wallet(
|
||||
currency,
|
||||
balances[currency].get("free", 0),
|
||||
balances[currency].get("used", 0),
|
||||
balances[currency].get("total", 0),
|
||||
)
|
||||
# Remove currencies no longer in get_balances output
|
||||
for currency in deepcopy(self._wallets):
|
||||
if currency not in balances:
|
||||
del self._wallets[currency]
|
||||
|
||||
positions = self._exchange.fetch_positions()
|
||||
_parsed_positions = {}
|
||||
@@ -176,6 +206,7 @@ class Wallets:
|
||||
side=position["side"],
|
||||
)
|
||||
self._positions = _parsed_positions
|
||||
self._wallets = _wallets
|
||||
|
||||
def update(self, require_update: bool = True) -> None:
|
||||
"""
|
||||
@@ -244,8 +275,10 @@ class Wallets:
|
||||
else:
|
||||
tot_profit = Trade.get_total_closed_profit()
|
||||
open_stakes = Trade.total_open_trades_stakes()
|
||||
available_balance = self.get_free(self._config["stake_currency"])
|
||||
return available_balance - tot_profit + open_stakes
|
||||
available_balance = self.get_free(self._stake_currency)
|
||||
return (available_balance - tot_profit + open_stakes) * self._config[
|
||||
"tradable_balance_ratio"
|
||||
]
|
||||
|
||||
def get_total_stake_amount(self):
|
||||
"""
|
||||
@@ -264,9 +297,9 @@ class Wallets:
|
||||
# Ensure <tradable_balance_ratio>% is used from the overall balance
|
||||
# Otherwise we'd risk lowering stakes with each open trade.
|
||||
# (tied up + current free) * ratio) - tied up
|
||||
available_amount = (
|
||||
val_tied_up + self.get_free(self._config["stake_currency"])
|
||||
) * self._config["tradable_balance_ratio"]
|
||||
available_amount = (val_tied_up + self.get_free(self._stake_currency)) * self._config[
|
||||
"tradable_balance_ratio"
|
||||
]
|
||||
return available_amount
|
||||
|
||||
def get_available_stake_amount(self) -> float:
|
||||
@@ -277,7 +310,7 @@ class Wallets:
|
||||
(<open_trade stakes> + free amount) * tradable_balance_ratio - <open_trade stakes>
|
||||
"""
|
||||
|
||||
free = self.get_free(self._config["stake_currency"])
|
||||
free = self.get_free(self._stake_currency)
|
||||
return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free)
|
||||
|
||||
def _calculate_unlimited_stake_amount(
|
||||
@@ -316,7 +349,7 @@ class Wallets:
|
||||
f"lower than stake amount ({stake_amount} {self._config['stake_currency']})"
|
||||
)
|
||||
|
||||
return stake_amount
|
||||
return max(stake_amount, 0)
|
||||
|
||||
def get_trade_stake_amount(
|
||||
self, pair: str, max_open_trades: IntOrInf, edge=None, update: bool = True
|
||||
@@ -336,8 +369,8 @@ class Wallets:
|
||||
if edge:
|
||||
stake_amount = edge.stake_amount(
|
||||
pair,
|
||||
self.get_free(self._config["stake_currency"]),
|
||||
self.get_total(self._config["stake_currency"]),
|
||||
self.get_free(self._stake_currency),
|
||||
self.get_total(self._stake_currency),
|
||||
val_tied_up,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from freqtrade_client.ft_rest_client import FtRestClient
|
||||
|
||||
|
||||
__version__ = "2024.11-dev"
|
||||
__version__ = "2024.12-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
+2
-1
@@ -111,6 +111,8 @@ develop = [
|
||||
"pytest-cov",
|
||||
"pytest-mock",
|
||||
"pytest-random-order",
|
||||
"pytest-timeout",
|
||||
"pytest-xdist",
|
||||
"pytest",
|
||||
"ruff",
|
||||
"time-machine",
|
||||
@@ -215,7 +217,6 @@ exclude = [
|
||||
"build_helpers/*.py",
|
||||
"ft_client/build/*",
|
||||
"build/*",
|
||||
"tests/*",
|
||||
]
|
||||
ignore = ["freqtrade/vendor/**"]
|
||||
pythonPlatform = "All"
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==4.0.1
|
||||
ruff==0.8.0
|
||||
ruff==0.8.3
|
||||
mypy==1.13.0
|
||||
pre-commit==4.0.1
|
||||
pytest==8.3.3
|
||||
pytest-asyncio==0.24.0
|
||||
pytest==8.3.4
|
||||
pytest-asyncio==0.25.0
|
||||
pytest-cov==6.0.0
|
||||
pytest-mock==3.14.0
|
||||
pytest-random-order==1.1.1
|
||||
@@ -28,5 +28,5 @@ nbconvert==7.16.4
|
||||
types-cachetools==5.5.0.20240820
|
||||
types-filelock==3.2.7
|
||||
types-requests==2.32.0.20241016
|
||||
types-tabulate==0.9.0.20240106
|
||||
types-python-dateutil==2.9.0.20241003
|
||||
types-tabulate==0.9.0.20241207
|
||||
types-python-dateutil==2.9.0.20241206
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
-r requirements-plot.txt
|
||||
|
||||
# Required for freqai
|
||||
scikit-learn==1.5.2
|
||||
scikit-learn==1.6.0
|
||||
joblib==1.4.2
|
||||
catboost==1.2.7; 'arm' not in platform_machine
|
||||
# Pin Matplotlib - it's depended on by catboost
|
||||
# Temporary downgrade of matplotlib due to https://github.com/matplotlib/matplotlib/issues/28551
|
||||
matplotlib==3.9.2
|
||||
matplotlib==3.10.0
|
||||
lightgbm==4.5.0
|
||||
xgboost==2.0.3
|
||||
xgboost==2.1.3
|
||||
tensorboard==2.18.0
|
||||
datasieve==0.1.7
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
# Required for hyperopt
|
||||
scipy==1.14.1
|
||||
scikit-learn==1.5.2
|
||||
scikit-learn==1.6.0
|
||||
ft-scikit-optimize==0.9.2
|
||||
filelock==3.16.1
|
||||
|
||||
+13
-10
@@ -4,12 +4,12 @@ bottleneck==1.4.2
|
||||
numexpr==2.10.2
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==4.4.35
|
||||
ccxt==4.4.40
|
||||
cryptography==42.0.8; platform_machine == 'armv7l'
|
||||
cryptography==43.0.3; platform_machine != 'armv7l'
|
||||
cryptography==44.0.0; platform_machine != 'armv7l'
|
||||
aiohttp==3.10.11
|
||||
SQLAlchemy==2.0.36
|
||||
python-telegram-bot==21.7
|
||||
python-telegram-bot==21.9
|
||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||
httpx>=0.24.1
|
||||
humanize==4.11.0
|
||||
@@ -17,15 +17,18 @@ cachetools==5.5.0
|
||||
requests==2.32.3
|
||||
urllib3==2.2.3
|
||||
jsonschema==4.23.0
|
||||
TA-Lib==0.4.32
|
||||
TA-Lib==0.4.34
|
||||
technical==1.4.4
|
||||
tabulate==0.9.0
|
||||
pycoingecko==3.2.0
|
||||
jinja2==3.1.4
|
||||
tables==3.10.1
|
||||
# Pin blosc2 to < 3.0 until piwheel has wheels for 3.x
|
||||
blosc2==2.7.1; platform_machine == 'armv7l' or python_version < '3.11'
|
||||
blosc2==3.0.0; platform_machine != 'armv7l' and python_version >= '3.11'
|
||||
joblib==1.4.2
|
||||
rich==13.9.4
|
||||
pyarrow==18.0.0; platform_machine != 'armv7l'
|
||||
pyarrow==18.1.0; platform_machine != 'armv7l'
|
||||
|
||||
# find first, C search in arrays
|
||||
py_find_1st==1.1.6
|
||||
@@ -39,10 +42,10 @@ orjson==3.10.12
|
||||
sdnotify==0.3.2
|
||||
|
||||
# API Server
|
||||
fastapi==0.115.5
|
||||
pydantic==2.10.1
|
||||
uvicorn==0.32.1
|
||||
pyjwt==2.10.0
|
||||
fastapi==0.115.6
|
||||
pydantic==2.10.3
|
||||
uvicorn==0.34.0
|
||||
pyjwt==2.10.1
|
||||
aiofiles==24.1.0
|
||||
psutil==6.1.0
|
||||
|
||||
@@ -58,7 +61,7 @@ schedule==1.2.2
|
||||
|
||||
#WS Messages
|
||||
websockets==14.1
|
||||
janus==1.1.0
|
||||
janus==2.0.0
|
||||
|
||||
ast-comments==1.2.2
|
||||
packaging==24.2
|
||||
|
||||
+27
-2
@@ -1,6 +1,7 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import json
|
||||
import logging
|
||||
import platform
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -517,6 +518,30 @@ def patch_gc(mocker) -> None:
|
||||
mocker.patch("freqtrade.main.gc_set_threshold")
|
||||
|
||||
|
||||
def is_arm() -> bool:
|
||||
machine = platform.machine()
|
||||
return "arm" in machine or "aarch64" in machine
|
||||
|
||||
|
||||
def is_mac() -> bool:
|
||||
machine = platform.system()
|
||||
return "Darwin" in machine
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_torch_initlogs(mocker) -> None:
|
||||
if is_mac():
|
||||
# Mock torch import completely
|
||||
import sys
|
||||
import types
|
||||
|
||||
module_name = "torch"
|
||||
mocked_module = types.ModuleType(module_name)
|
||||
sys.modules[module_name] = mocked_module
|
||||
else:
|
||||
mocker.patch("torch._logging._init_logs")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def user_dir(mocker, tmp_path) -> Path:
|
||||
user_dir = tmp_path / "user_data"
|
||||
@@ -2235,7 +2260,7 @@ def tickers():
|
||||
"first": None,
|
||||
"last": 8603.67,
|
||||
"change": -0.879,
|
||||
"percentage": None,
|
||||
"percentage": -8.95,
|
||||
"average": None,
|
||||
"baseVolume": 30414.604298,
|
||||
"quoteVolume": 259629896.48584127,
|
||||
@@ -2279,7 +2304,7 @@ def tickers():
|
||||
"first": None,
|
||||
"last": 129.28,
|
||||
"change": 1.795,
|
||||
"percentage": None,
|
||||
"percentage": -2.5,
|
||||
"average": None,
|
||||
"baseVolume": 59698.79897,
|
||||
"quoteVolume": 29132399.743954,
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from freqtrade.constants import DEFAULT_TRADES_COLUMNS
|
||||
from freqtrade.data.converter import populate_dataframe_with_trades
|
||||
from freqtrade.data.converter.orderflow import trades_to_volumeprofile_with_total_delta_bid_ask
|
||||
from freqtrade.data.converter.orderflow import (
|
||||
ORDERFLOW_ADDED_COLUMNS,
|
||||
timeframe_to_DateOffset,
|
||||
trades_to_volumeprofile_with_total_delta_bid_ask,
|
||||
)
|
||||
from freqtrade.data.converter.trade_converter import trades_list_to_df
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from tests.strategy.strats.strategy_test_v3 import StrategyTestV3
|
||||
|
||||
|
||||
BIN_SIZE_SCALE = 0.5
|
||||
@@ -37,6 +41,7 @@ def populate_dataframe_with_trades_trades(testdatadir):
|
||||
|
||||
@pytest.fixture
|
||||
def candles(testdatadir):
|
||||
# TODO: this fixture isn't really necessary and could be removed
|
||||
return pd.read_json(testdatadir / "orderflow/candles.json").copy()
|
||||
|
||||
|
||||
@@ -102,7 +107,7 @@ def test_public_trades_mock_populate_dataframe_with_trades__check_orderflow(
|
||||
},
|
||||
}
|
||||
# Apply the function to populate the data frame with order flow data
|
||||
df, _ = populate_dataframe_with_trades(OrderedDict(), config, dataframe, trades)
|
||||
df, _ = populate_dataframe_with_trades(None, config, dataframe, trades)
|
||||
# Extract results from the first row of the DataFrame
|
||||
results = df.iloc[0]
|
||||
t = results["trades"]
|
||||
@@ -243,7 +248,7 @@ def test_public_trades_trades_mock_populate_dataframe_with_trades__check_trades(
|
||||
}
|
||||
|
||||
# Populate the DataFrame with trades and order flow data
|
||||
df, _ = populate_dataframe_with_trades(OrderedDict(), config, dataframe, trades)
|
||||
df, _ = populate_dataframe_with_trades(None, config, dataframe, trades)
|
||||
|
||||
# --- DataFrame and Trade Data Validation ---
|
||||
|
||||
@@ -401,9 +406,7 @@ def test_public_trades_config_max_trades(
|
||||
},
|
||||
}
|
||||
|
||||
df, _ = populate_dataframe_with_trades(
|
||||
OrderedDict(), default_conf | orderflow_config, dataframe, trades
|
||||
)
|
||||
df, _ = populate_dataframe_with_trades(None, default_conf | orderflow_config, dataframe, trades)
|
||||
assert df.delta.count() == 1
|
||||
|
||||
|
||||
@@ -482,3 +485,94 @@ def test_public_trades_testdata_sanity(
|
||||
"cost",
|
||||
"date",
|
||||
]
|
||||
|
||||
|
||||
def test_analyze_with_orderflow(
|
||||
default_conf_usdt,
|
||||
mocker,
|
||||
populate_dataframe_with_trades_dataframe,
|
||||
populate_dataframe_with_trades_trades,
|
||||
):
|
||||
ohlcv_history = populate_dataframe_with_trades_dataframe
|
||||
# call without orderflow
|
||||
strategy = StrategyTestV3(config=default_conf_usdt)
|
||||
strategy.dp = DataProvider(default_conf_usdt, None, None)
|
||||
|
||||
mocker.patch.object(strategy.dp, "trades", return_value=populate_dataframe_with_trades_trades)
|
||||
import freqtrade.data.converter.orderflow as orderflow_module
|
||||
|
||||
spy = mocker.spy(orderflow_module, "trades_to_volumeprofile_with_total_delta_bid_ask")
|
||||
|
||||
pair = "ETH/BTC"
|
||||
df = strategy.advise_indicators(ohlcv_history, {"pair:": pair})
|
||||
assert len(df) == len(ohlcv_history)
|
||||
assert "open" in df.columns
|
||||
assert spy.call_count == 0
|
||||
|
||||
# Not expected to run - shouldn't have added orderflow columns
|
||||
for col in ORDERFLOW_ADDED_COLUMNS:
|
||||
assert col not in df.columns, f"Column {col} found in df.columns"
|
||||
|
||||
default_conf_usdt["exchange"]["use_public_trades"] = True
|
||||
default_conf_usdt["orderflow"] = {
|
||||
"cache_size": 5,
|
||||
"max_candles": 5,
|
||||
"scale": 0.005,
|
||||
"imbalance_volume": 0,
|
||||
"imbalance_ratio": 3,
|
||||
"stacked_imbalance_range": 3,
|
||||
}
|
||||
|
||||
strategy.config = default_conf_usdt
|
||||
# First round - builds cache
|
||||
df1 = strategy.advise_indicators(ohlcv_history, {"pair": pair})
|
||||
assert len(df1) == len(ohlcv_history)
|
||||
assert "open" in df1.columns
|
||||
assert spy.call_count == 5
|
||||
|
||||
for col in ORDERFLOW_ADDED_COLUMNS:
|
||||
assert col in df1.columns, f"Column {col} not found in df.columns"
|
||||
|
||||
if col not in ("stacked_imbalances_bid", "stacked_imbalances_ask"):
|
||||
assert df1[col].count() == 5, f"Column {col} has {df1[col].count()} non-NaN values"
|
||||
|
||||
assert len(strategy._cached_grouped_trades_per_pair[pair]) == 5
|
||||
|
||||
lastval_trades = df1.at[len(df1) - 1, "trades"]
|
||||
assert isinstance(lastval_trades, list)
|
||||
assert len(lastval_trades) == 122
|
||||
|
||||
lastval_of = df1.at[len(df1) - 1, "orderflow"]
|
||||
assert isinstance(lastval_of, dict)
|
||||
|
||||
spy.reset_mock()
|
||||
# Ensure caching works - call the same logic again.
|
||||
df2 = strategy.advise_indicators(ohlcv_history, {"pair": pair})
|
||||
assert len(df2) == len(ohlcv_history)
|
||||
assert "open" in df2.columns
|
||||
assert spy.call_count == 0
|
||||
for col in ORDERFLOW_ADDED_COLUMNS:
|
||||
assert col in df2.columns, f"Round2: Column {col} not found in df.columns"
|
||||
|
||||
if col not in ("stacked_imbalances_bid", "stacked_imbalances_ask"):
|
||||
assert (
|
||||
df2[col].count() == 5
|
||||
), f"Round2: Column {col} has {df2[col].count()} non-NaN values"
|
||||
|
||||
lastval_trade2 = df2.at[len(df2) - 1, "trades"]
|
||||
assert isinstance(lastval_trade2, list)
|
||||
assert len(lastval_trade2) == 122
|
||||
|
||||
lastval_of2 = df2.at[len(df2) - 1, "orderflow"]
|
||||
assert isinstance(lastval_of2, dict)
|
||||
|
||||
|
||||
def test_timeframe_to_DateOffset():
|
||||
assert timeframe_to_DateOffset("1s") == pd.DateOffset(seconds=1)
|
||||
assert timeframe_to_DateOffset("1m") == pd.DateOffset(minutes=1)
|
||||
assert timeframe_to_DateOffset("5m") == pd.DateOffset(minutes=5)
|
||||
assert timeframe_to_DateOffset("1h") == pd.DateOffset(hours=1)
|
||||
assert timeframe_to_DateOffset("1d") == pd.DateOffset(days=1)
|
||||
assert timeframe_to_DateOffset("1w") == pd.DateOffset(weeks=1)
|
||||
assert timeframe_to_DateOffset("1M") == pd.DateOffset(months=1)
|
||||
assert timeframe_to_DateOffset("1y") == pd.DateOffset(years=1)
|
||||
|
||||
@@ -128,8 +128,8 @@ def test_load_data_with_new_pair_1min(
|
||||
"""
|
||||
Test load_pair_history() with 1 min timeframe
|
||||
"""
|
||||
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.object(exchange, "get_historic_ohlcv", return_value=ohlcv_history)
|
||||
file = tmp_path / "MEME_BTC-1m.feather"
|
||||
|
||||
# do not download a new pair if refresh_pairs isn't set
|
||||
@@ -306,8 +306,8 @@ def test_load_cached_data_for_updating(testdatadir) -> None:
|
||||
def test_download_pair_history(
|
||||
ohlcv_history, mocker, default_conf, tmp_path, candle_type, subdir, file_tail
|
||||
) -> None:
|
||||
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.object(exchange, "get_historic_ohlcv", return_value=ohlcv_history)
|
||||
file1_1 = tmp_path / f"{subdir}MEME_BTC-1m{file_tail}.feather"
|
||||
file1_5 = tmp_path / f"{subdir}MEME_BTC-5m{file_tail}.feather"
|
||||
file2_1 = tmp_path / f"{subdir}CFI_BTC-1m{file_tail}.feather"
|
||||
@@ -357,8 +357,8 @@ def test_download_pair_history2(mocker, default_conf, testdatadir, ohlcv_history
|
||||
"freqtrade.data.history.datahandlers.featherdatahandler.FeatherDataHandler.ohlcv_store",
|
||||
return_value=None,
|
||||
)
|
||||
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.object(exchange, "get_historic_ohlcv", return_value=ohlcv_history)
|
||||
_download_pair_history(
|
||||
datadir=testdatadir,
|
||||
exchange=exchange,
|
||||
|
||||
+237
-32
@@ -1,14 +1,17 @@
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import ccxt
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_seconds
|
||||
from freqtrade.persistence import Trade
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts, dt_utc
|
||||
from tests.conftest import EXMS, get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
@@ -290,6 +293,7 @@ def test_liquidation_price_binance(
|
||||
default_conf["trading_mode"] = trading_mode
|
||||
default_conf["margin_mode"] = margin_mode
|
||||
default_conf["liquidation_buffer"] = 0.0
|
||||
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
|
||||
|
||||
def get_maint_ratio(pair_, stake_amount):
|
||||
@@ -731,42 +735,243 @@ def test__set_leverage_binance(mocker, default_conf):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("candle_type", [CandleType.MARK, ""])
|
||||
async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, candle_type):
|
||||
ohlcv = [
|
||||
[
|
||||
int((datetime.now(timezone.utc).timestamp() - 1000) * 1000),
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
4, # close
|
||||
5, # volume (in quote currency)
|
||||
def patch_binance_vision_ohlcv(mocker, start, archive_end, api_end, timeframe):
|
||||
def make_storage(start: datetime, end: datetime, timeframe: str):
|
||||
date = pd.date_range(start, end, freq=timeframe.replace("m", "min"))
|
||||
df = pd.DataFrame(
|
||||
data=dict(date=date, open=1.0, high=1.0, low=1.0, close=1.0),
|
||||
)
|
||||
return df
|
||||
|
||||
archive_storage = make_storage(start, archive_end, timeframe)
|
||||
api_storage = make_storage(start, api_end, timeframe)
|
||||
|
||||
ohlcv = [[dt_ts(start), 1, 1, 1, 1]]
|
||||
# (pair, timeframe, candle_type, ohlcv, True)
|
||||
candle_history = [None, None, None, ohlcv, None]
|
||||
|
||||
def get_historic_ohlcv(
|
||||
# self,
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
since_ms: int,
|
||||
candle_type: CandleType,
|
||||
is_new_pair: bool = False,
|
||||
until_ms: int | None = None,
|
||||
):
|
||||
since = dt_from_ts(since_ms)
|
||||
until = dt_from_ts(until_ms) if until_ms else api_end + timedelta(seconds=1)
|
||||
return api_storage.loc[(api_storage["date"] >= since) & (api_storage["date"] < until)]
|
||||
|
||||
async def download_archive_ohlcv(
|
||||
candle_type,
|
||||
pair,
|
||||
timeframe,
|
||||
since_ms,
|
||||
until_ms,
|
||||
markets=None,
|
||||
stop_on_404=False,
|
||||
):
|
||||
since = dt_from_ts(since_ms)
|
||||
until = dt_from_ts(until_ms) if until_ms else archive_end + timedelta(seconds=1)
|
||||
if since < start:
|
||||
pass
|
||||
return archive_storage.loc[
|
||||
(archive_storage["date"] >= since) & (archive_storage["date"] < until)
|
||||
]
|
||||
]
|
||||
|
||||
candle_mock = mocker.patch(f"{EXMS}._async_get_candle_history", return_value=candle_history)
|
||||
api_mock = mocker.patch(f"{EXMS}.get_historic_ohlcv", side_effect=get_historic_ohlcv)
|
||||
archive_mock = mocker.patch(
|
||||
"freqtrade.exchange.binance.download_archive_ohlcv", side_effect=download_archive_ohlcv
|
||||
)
|
||||
return candle_mock, api_mock, archive_mock
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"timeframe,is_new_pair,since,until,first_date,last_date,candle_called,archive_called,"
|
||||
"api_called",
|
||||
[
|
||||
(
|
||||
"1m",
|
||||
True,
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 2),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23, 59),
|
||||
True,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
True,
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 3),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 2, 23, 59),
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
True,
|
||||
dt_utc(2020, 1, 2),
|
||||
dt_utc(2020, 1, 2, 1),
|
||||
dt_utc(2020, 1, 2),
|
||||
dt_utc(2020, 1, 2, 0, 59),
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
False,
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 2),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23, 59),
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
True,
|
||||
dt_utc(2019, 1, 1),
|
||||
dt_utc(2020, 1, 2),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23, 59),
|
||||
True,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
False,
|
||||
dt_utc(2019, 1, 1),
|
||||
dt_utc(2020, 1, 2),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23, 59),
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
False,
|
||||
dt_utc(2019, 1, 1),
|
||||
dt_utc(2019, 1, 2),
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
True,
|
||||
True,
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
True,
|
||||
dt_utc(2019, 1, 1),
|
||||
dt_utc(2019, 1, 2),
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
False,
|
||||
dt_utc(2021, 1, 1),
|
||||
dt_utc(2021, 1, 2),
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
True,
|
||||
dt_utc(2021, 1, 1),
|
||||
dt_utc(2021, 1, 2),
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"1h",
|
||||
False,
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 2),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23),
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
False,
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 3, 50, 30),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 3, 50),
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_get_historic_ohlcv_binance(
|
||||
mocker,
|
||||
default_conf,
|
||||
timeframe,
|
||||
is_new_pair,
|
||||
since,
|
||||
until,
|
||||
first_date,
|
||||
last_date,
|
||||
candle_called,
|
||||
archive_called,
|
||||
api_called,
|
||||
):
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
|
||||
# Monkey-patch async function
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
|
||||
pair = "ETH/BTC"
|
||||
respair, restf, restype, res, _ = await exchange._async_get_historic_ohlcv(
|
||||
pair, "5m", 1500000000000, is_new_pair=False, candle_type=candle_type
|
||||
)
|
||||
assert respair == pair
|
||||
assert restf == "5m"
|
||||
assert restype == candle_type
|
||||
# Call with very old timestamp - causes tons of requests
|
||||
assert exchange._api_async.fetch_ohlcv.call_count > 400
|
||||
# assert res == ohlcv
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
_, _, _, res, _ = await exchange._async_get_historic_ohlcv(
|
||||
pair, "5m", 1500000000000, is_new_pair=True, candle_type=candle_type
|
||||
start = dt_utc(2020, 1, 1)
|
||||
archive_end = dt_utc(2020, 1, 2)
|
||||
api_end = dt_utc(2020, 1, 3)
|
||||
candle_mock, api_mock, archive_mock = patch_binance_vision_ohlcv(
|
||||
mocker, start=start, archive_end=archive_end, api_end=api_end, timeframe=timeframe
|
||||
)
|
||||
|
||||
# Called twice - one "init" call - and one to get the actual data.
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert res == ohlcv
|
||||
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
|
||||
candle_type = CandleType.SPOT
|
||||
pair = "BTC/USDT"
|
||||
|
||||
since_ms = dt_ts(since)
|
||||
until_ms = dt_ts(until)
|
||||
|
||||
df = exchange.get_historic_ohlcv(pair, timeframe, since_ms, candle_type, is_new_pair, until_ms)
|
||||
|
||||
if df.empty:
|
||||
assert first_date is None
|
||||
assert last_date is None
|
||||
else:
|
||||
assert df["date"].iloc[0] == first_date
|
||||
assert df["date"].iloc[-1] == last_date
|
||||
assert (
|
||||
df["date"].diff().iloc[1:] == timedelta(seconds=timeframe_to_seconds(timeframe))
|
||||
).all()
|
||||
|
||||
if candle_called:
|
||||
candle_mock.assert_called_once()
|
||||
if archive_called:
|
||||
archive_mock.assert_called_once()
|
||||
if api_called:
|
||||
api_mock.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
from datetime import timedelta
|
||||
|
||||
import aiohttp
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exchange.binance_public_data import (
|
||||
BadHttpStatus,
|
||||
Http404,
|
||||
binance_vision_zip_name,
|
||||
download_archive_ohlcv,
|
||||
get_daily_ohlcv,
|
||||
)
|
||||
from freqtrade.util.datetime_helpers import dt_ts, dt_utc
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def event_loop_policy(request):
|
||||
if sys.platform == "win32":
|
||||
return asyncio.WindowsSelectorEventLoopPolicy()
|
||||
else:
|
||||
return asyncio.DefaultEventLoopPolicy()
|
||||
|
||||
|
||||
class MockResponse:
|
||||
"""AioHTTP response mock"""
|
||||
|
||||
def __init__(self, content, status, reason=""):
|
||||
self._content = content
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
|
||||
async def read(self):
|
||||
return self._content
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
|
||||
# spot klines archive csv file format, the futures/um klines don't have the header line
|
||||
#
|
||||
# open_time,open,high,low,close,volume,close_time,quote_volume,count,taker_buy_volume,taker_buy_quote_volume,ignore # noqa: E501
|
||||
# 1698364800000,34161.6,34182.5,33977.4,34024.2,409953,1698368399999,1202.97118037,15095,192220,564.12041453,0 # noqa: E501
|
||||
# 1698368400000,34024.2,34060.1,33776.4,33848.4,740960,1698371999999,2183.75671155,23938,368266,1085.17080793,0 # noqa: E501
|
||||
# 1698372000000,33848.5,34150.0,33815.1,34094.2,390376,1698375599999,1147.73267094,13854,231446,680.60405822,0 # noqa: E501
|
||||
|
||||
|
||||
def make_response_from_url(start_date, end_date):
|
||||
def make_daily_df(date, timeframe):
|
||||
start = dt_utc(date.year, date.month, date.day)
|
||||
end = start + timedelta(days=1)
|
||||
date_col = pd.date_range(start, end, freq=timeframe.replace("m", "min"), inclusive="left")
|
||||
cols = (
|
||||
"open_time,open,high,low,close,volume,close_time,quote_volume,count,taker_buy_volume,"
|
||||
"taker_buy_quote_volume,ignore"
|
||||
)
|
||||
df = pd.DataFrame(columns=cols.split(","), dtype=float)
|
||||
df["open_time"] = date_col.astype("int64") // 10**6
|
||||
df["open"] = df["high"] = df["low"] = df["close"] = df["volume"] = 1.0
|
||||
return df
|
||||
|
||||
def make_daily_zip(asset_type_url_segment, symbol, timeframe, date) -> bytes:
|
||||
df = make_daily_df(date, timeframe)
|
||||
if asset_type_url_segment == "spot":
|
||||
header = True
|
||||
elif asset_type_url_segment == "futures/um":
|
||||
header = None
|
||||
else:
|
||||
raise ValueError
|
||||
csv = df.to_csv(index=False, header=header)
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, "w") as zipf:
|
||||
zipf.writestr(binance_vision_zip_name(symbol, timeframe, date), csv)
|
||||
return zip_buffer.getvalue()
|
||||
|
||||
def make_response(url):
|
||||
pattern = (
|
||||
r"https://data.binance.vision/data/(?P<asset_type_url_segment>spot|futures/um)"
|
||||
r"/daily/klines/(?P<symbol>.*?)/(?P<timeframe>.*?)/(?P=symbol)-(?P=timeframe)-"
|
||||
r"(?P<date>\d{4}-\d{2}-\d{2}).zip"
|
||||
)
|
||||
m = re.match(pattern, url)
|
||||
if not m:
|
||||
return MockResponse(content="", status=404)
|
||||
|
||||
date = datetime.datetime.strptime(m["date"], "%Y-%m-%d").date()
|
||||
if date < start_date or date > end_date:
|
||||
return MockResponse(content="", status=404)
|
||||
|
||||
zip_file = make_daily_zip(m["asset_type_url_segment"], m["symbol"], m["timeframe"], date)
|
||||
return MockResponse(content=zip_file, status=200)
|
||||
|
||||
return make_response
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"candle_type,pair,since,until,first_date,last_date,stop_on_404",
|
||||
[
|
||||
(
|
||||
CandleType.SPOT,
|
||||
"BTC/USDT",
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 2),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CandleType.SPOT,
|
||||
"BTC/USDT",
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23, 59, 59),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CandleType.SPOT,
|
||||
"BTC/USDT",
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 5),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 3, 23),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CandleType.SPOT,
|
||||
"BTC/USDT",
|
||||
dt_utc(2019, 12, 25),
|
||||
dt_utc(2020, 1, 5),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 3, 23),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CandleType.SPOT,
|
||||
"BTC/USDT",
|
||||
dt_utc(2019, 1, 1),
|
||||
dt_utc(2019, 1, 5),
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
),
|
||||
(
|
||||
CandleType.SPOT,
|
||||
"BTC/USDT",
|
||||
dt_utc(2021, 1, 1),
|
||||
dt_utc(2021, 1, 5),
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
),
|
||||
(
|
||||
CandleType.SPOT,
|
||||
"BTC/USDT",
|
||||
dt_utc(2020, 1, 2),
|
||||
None,
|
||||
dt_utc(2020, 1, 2),
|
||||
dt_utc(2020, 1, 3, 23),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CandleType.SPOT,
|
||||
"BTC/USDT",
|
||||
dt_utc(2020, 1, 5),
|
||||
dt_utc(2020, 1, 1),
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
),
|
||||
(
|
||||
CandleType.FUTURES,
|
||||
"BTC/USDT:USDT",
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23, 59, 59),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CandleType.INDEX,
|
||||
"N/A",
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 1, 23, 59, 59),
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
),
|
||||
# stop_on_404 = True
|
||||
(
|
||||
CandleType.SPOT,
|
||||
"BTC/USDT",
|
||||
dt_utc(2019, 12, 25),
|
||||
dt_utc(2020, 1, 5),
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
),
|
||||
(
|
||||
CandleType.SPOT,
|
||||
"BTC/USDT",
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 5),
|
||||
dt_utc(2020, 1, 1),
|
||||
dt_utc(2020, 1, 3, 23),
|
||||
True,
|
||||
),
|
||||
(
|
||||
CandleType.FUTURES,
|
||||
"BTC/USDT:USDT",
|
||||
dt_utc(2019, 12, 25),
|
||||
dt_utc(2020, 1, 5),
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_download_archive_ohlcv(
|
||||
mocker, candle_type, pair, since, until, first_date, last_date, stop_on_404
|
||||
):
|
||||
history_start = dt_utc(2020, 1, 1).date()
|
||||
history_end = dt_utc(2020, 1, 3).date()
|
||||
timeframe = "1h"
|
||||
|
||||
since_ms = dt_ts(since)
|
||||
until_ms = dt_ts(until)
|
||||
|
||||
mocker.patch(
|
||||
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
|
||||
side_effect=make_response_from_url(history_start, history_end),
|
||||
)
|
||||
markets = {"BTC/USDT": {"id": "BTCUSDT"}, "BTC/USDT:USDT": {"id": "BTCUSDT"}}
|
||||
|
||||
df = await download_archive_ohlcv(
|
||||
candle_type,
|
||||
pair,
|
||||
timeframe,
|
||||
since_ms=since_ms,
|
||||
until_ms=until_ms,
|
||||
markets=markets,
|
||||
stop_on_404=stop_on_404,
|
||||
)
|
||||
|
||||
if df.empty:
|
||||
assert first_date is None and last_date is None
|
||||
else:
|
||||
assert candle_type in [CandleType.SPOT, CandleType.FUTURES]
|
||||
assert df["date"].iloc[0] == first_date
|
||||
assert df["date"].iloc[-1] == last_date
|
||||
|
||||
|
||||
async def test_download_archive_ohlcv_exception(mocker):
|
||||
timeframe = "1h"
|
||||
pair = "BTC/USDT"
|
||||
|
||||
since_ms = dt_ts(dt_utc(2020, 1, 1))
|
||||
until_ms = dt_ts(dt_utc(2020, 1, 2))
|
||||
|
||||
markets = {"BTC/USDT": {"id": "BTCUSDT"}, "BTC/USDT:USDT": {"id": "BTCUSDT"}}
|
||||
mocker.patch(
|
||||
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get", side_effect=RuntimeError
|
||||
)
|
||||
|
||||
df = await download_archive_ohlcv(
|
||||
CandleType.SPOT, pair, timeframe, since_ms=since_ms, until_ms=until_ms, markets=markets
|
||||
)
|
||||
|
||||
assert df.empty
|
||||
|
||||
|
||||
async def test_get_daily_ohlcv(mocker, testdatadir):
|
||||
symbol = "BTCUSDT"
|
||||
timeframe = "1h"
|
||||
date = dt_utc(2024, 10, 28).date()
|
||||
first_date = dt_utc(2024, 10, 28)
|
||||
last_date = dt_utc(2024, 10, 28, 23)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
spot_path = (
|
||||
testdatadir / "binance/binance_public_data/spot-klines-BTCUSDT-1h-2024-10-28.zip"
|
||||
)
|
||||
get = mocker.patch(
|
||||
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
|
||||
return_value=MockResponse(spot_path.read_bytes(), 200),
|
||||
)
|
||||
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session)
|
||||
assert get.call_count == 1
|
||||
assert df["date"].iloc[0] == first_date
|
||||
assert df["date"].iloc[-1] == last_date
|
||||
|
||||
futures_path = (
|
||||
testdatadir / "binance/binance_public_data/futures-um-klines-BTCUSDT-1h-2024-10-28.zip"
|
||||
)
|
||||
get = mocker.patch(
|
||||
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
|
||||
return_value=MockResponse(futures_path.read_bytes(), 200),
|
||||
)
|
||||
df = await get_daily_ohlcv("futures/um", symbol, timeframe, date, session)
|
||||
assert get.call_count == 1
|
||||
assert df["date"].iloc[0] == first_date
|
||||
assert df["date"].iloc[-1] == last_date
|
||||
|
||||
get = mocker.patch(
|
||||
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
|
||||
return_value=MockResponse(b"", 404),
|
||||
)
|
||||
with pytest.raises(Http404):
|
||||
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session, retry_delay=0)
|
||||
assert get.call_count == 1
|
||||
|
||||
get = mocker.patch(
|
||||
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
|
||||
return_value=MockResponse(b"", 500),
|
||||
)
|
||||
mocker.patch("asyncio.sleep")
|
||||
with pytest.raises(BadHttpStatus):
|
||||
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session)
|
||||
assert get.call_count == 4 # 1 + 3 default retries
|
||||
|
||||
get = mocker.patch(
|
||||
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
|
||||
return_value=MockResponse(b"nop", 200),
|
||||
)
|
||||
with pytest.raises(zipfile.BadZipFile):
|
||||
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session)
|
||||
assert get.call_count == 4 # 1 + 3 default retries
|
||||
@@ -2006,6 +2006,46 @@ def test_get_tickers(default_conf, mocker, exchange_name, caplog):
|
||||
assert exchange.get_tickers() == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
tick = {
|
||||
"ETH/USDT": {
|
||||
"last": 42,
|
||||
},
|
||||
"BCH/USDT": {
|
||||
"last": 41,
|
||||
},
|
||||
"ETH/BTC": {
|
||||
"last": 250,
|
||||
},
|
||||
}
|
||||
tick2 = {
|
||||
"ADA/USDT:USDT": {
|
||||
"last": 2.5,
|
||||
}
|
||||
}
|
||||
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||
api_mock.fetch_tickers = MagicMock(side_effect=[tick, tick2])
|
||||
api_mock.fetch_bids_asks = MagicMock(return_value={})
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange=exchange_name)
|
||||
# retrieve original ticker
|
||||
assert exchange.get_conversion_rate("USDT", "USDT") == 1
|
||||
assert api_mock.fetch_tickers.call_count == 0
|
||||
assert exchange.get_conversion_rate("ETH", "USDT") == 42
|
||||
assert exchange.get_conversion_rate("ETH", "USDC") is None
|
||||
assert exchange.get_conversion_rate("ETH", "BTC") == 250
|
||||
assert exchange.get_conversion_rate("BTC", "ETH") == 0.004
|
||||
|
||||
assert api_mock.fetch_tickers.call_count == 1
|
||||
api_mock.fetch_tickers.reset_mock()
|
||||
|
||||
assert exchange.get_conversion_rate("ADA", "USDT") == 2.5
|
||||
# Only the call to the "others" market
|
||||
assert api_mock.fetch_tickers.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_fetch_ticker(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
@@ -2091,6 +2131,7 @@ def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_mach
|
||||
@pytest.mark.parametrize("candle_type", ["mark", ""])
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
|
||||
pair = "ETH/BTC"
|
||||
calls = 0
|
||||
@@ -2123,7 +2164,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
|
||||
assert exchange._async_get_candle_history.call_count == 2
|
||||
# Returns twice the above OHLCV data after truncating the open candle.
|
||||
assert len(ret) == 2
|
||||
assert log_has_re(r"Downloaded data for .* with length .*\.", caplog)
|
||||
assert log_has_re(r"Downloaded data for .* from ccxt with length .*\.", caplog)
|
||||
|
||||
caplog.clear()
|
||||
|
||||
@@ -2156,7 +2197,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
|
||||
|
||||
pair = "ETH/USDT"
|
||||
respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv(
|
||||
pair, "5m", 1500000000000, candle_type=candle_type, is_new_pair=False
|
||||
pair, "5m", 1500000000000, candle_type=candle_type
|
||||
)
|
||||
assert respair == pair
|
||||
assert restf == "5m"
|
||||
@@ -2168,7 +2209,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
|
||||
end_ts = 1_500_500_000_000
|
||||
start_ts = 1_500_000_000_000
|
||||
respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv(
|
||||
pair, "5m", since_ms=start_ts, candle_type=candle_type, is_new_pair=False, until_ms=end_ts
|
||||
pair, "5m", since_ms=start_ts, candle_type=candle_type, until_ms=end_ts
|
||||
)
|
||||
# Required candles
|
||||
candles = (end_ts - start_ts) / 300_000
|
||||
@@ -4078,10 +4119,16 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
|
||||
)
|
||||
ex = Exchange(default_conf)
|
||||
|
||||
assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC"
|
||||
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
|
||||
assert next(ex.get_valid_pair_combination("ETH", "BTC")) == "ETH/BTC"
|
||||
assert next(ex.get_valid_pair_combination("BTC", "ETH")) == "ETH/BTC"
|
||||
multicombs = list(ex.get_valid_pair_combination("ETH", "USDT"))
|
||||
assert len(multicombs) == 2
|
||||
assert "ETH/USDT" in multicombs
|
||||
assert "ETH/USDT:USDT" in multicombs
|
||||
|
||||
with pytest.raises(ValueError, match=r"Could not combine.* to get a valid pair."):
|
||||
ex.get_valid_pair_combination("NOPAIR", "ETH")
|
||||
for x in ex.get_valid_pair_combination("NOPAIR", "ETH"):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -6130,6 +6177,7 @@ def test_get_liquidation_price(
|
||||
default_conf_usdt["exchange"]["name"] = exchange_name
|
||||
default_conf_usdt["margin_mode"] = margin_mode
|
||||
mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes")
|
||||
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name)
|
||||
|
||||
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import platform
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
@@ -20,30 +19,6 @@ def is_py12() -> bool:
|
||||
return sys.version_info >= (3, 12)
|
||||
|
||||
|
||||
def is_mac() -> bool:
|
||||
machine = platform.system()
|
||||
return "Darwin" in machine
|
||||
|
||||
|
||||
def is_arm() -> bool:
|
||||
machine = platform.machine()
|
||||
return "arm" in machine or "aarch64" in machine
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_torch_initlogs(mocker) -> None:
|
||||
if is_mac():
|
||||
# Mock torch import completely
|
||||
import sys
|
||||
import types
|
||||
|
||||
module_name = "torch"
|
||||
mocked_module = types.ModuleType(module_name)
|
||||
sys.modules[module_name] = mocked_module
|
||||
else:
|
||||
mocker.patch("torch._logging._init_logs")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def freqai_conf(default_conf, tmp_path):
|
||||
freqaiconf = deepcopy(default_conf)
|
||||
|
||||
@@ -10,11 +10,10 @@ from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.conftest import get_patched_exchange, is_mac
|
||||
from tests.freqai.conftest import (
|
||||
get_patched_data_kitchen,
|
||||
get_patched_freqai_strategy,
|
||||
is_mac,
|
||||
make_unfiltered_dataframe,
|
||||
)
|
||||
|
||||
|
||||
@@ -13,11 +13,16 @@ from freqtrade.freqai.utils import download_all_data_for_training, get_required_
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re
|
||||
from tests.freqai.conftest import (
|
||||
get_patched_freqai_strategy,
|
||||
from tests.conftest import (
|
||||
EXMS,
|
||||
create_mock_trades,
|
||||
get_patched_exchange,
|
||||
is_arm,
|
||||
is_mac,
|
||||
log_has_re,
|
||||
)
|
||||
from tests.freqai.conftest import (
|
||||
get_patched_freqai_strategy,
|
||||
make_rl_config,
|
||||
mock_pytorch_mlp_model_training_parameters,
|
||||
)
|
||||
|
||||
@@ -4022,7 +4022,7 @@ def test_get_real_amount_fees_order(
|
||||
default_conf_usdt, market_buy_order_usdt_doublefee, fee, mocker
|
||||
):
|
||||
tfo_mock = mocker.patch(f"{EXMS}.get_trades_for_order", return_value=[])
|
||||
mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value="BNB/USDT")
|
||||
mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value=["BNB/USDT"])
|
||||
mocker.patch(f"{EXMS}.fetch_ticker", return_value={"last": 200})
|
||||
trade = Trade(
|
||||
pair="LTC/USDT",
|
||||
@@ -5191,6 +5191,13 @@ def test_update_funding_fees(
|
||||
open_exit_order = limit_order_open[exit_side(is_short)]
|
||||
bid = 0.11
|
||||
enter_rate_mock = MagicMock(return_value=bid)
|
||||
open_order.update(
|
||||
{
|
||||
"status": "closed",
|
||||
"filled": open_order["amount"],
|
||||
"remaining": 0,
|
||||
}
|
||||
)
|
||||
enter_mm = MagicMock(return_value=open_order)
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_update_liquidation_prices(mocker, margin_mode, dry_run):
|
||||
|
||||
assert trade_mock.set_liquidation_price.call_count == 1
|
||||
|
||||
assert wallets.get_total.call_count == (
|
||||
assert wallets.get_collateral.call_count == (
|
||||
0 if margin_mode == MarginMode.ISOLATED or not dry_run else 1
|
||||
)
|
||||
|
||||
|
||||
@@ -371,8 +371,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||
mocker.patch("freqtrade.optimize.backtesting.Backtesting.backtest")
|
||||
mocker.patch("freqtrade.optimize.backtesting.generate_backtest_stats")
|
||||
mocker.patch("freqtrade.optimize.backtesting.show_backtest_results")
|
||||
sbs = mocker.patch("freqtrade.optimize.backtesting.store_backtest_stats")
|
||||
sbc = mocker.patch("freqtrade.optimize.backtesting.store_backtest_analysis_results")
|
||||
sbs = mocker.patch("freqtrade.optimize.backtesting.store_backtest_results")
|
||||
mocker.patch(
|
||||
"freqtrade.plugins.pairlistmanager.PairListManager.whitelist",
|
||||
PropertyMock(return_value=["UNITTEST/BTC"]),
|
||||
@@ -397,7 +396,6 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||
assert backtesting.strategy.bot_start.call_count == 1
|
||||
assert backtesting.strategy.bot_loop_start.call_count == 0
|
||||
assert sbs.call_count == 1
|
||||
assert sbc.call_count == 1
|
||||
|
||||
|
||||
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
|
||||
@@ -568,6 +566,9 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
|
||||
mocker.patch(f"{EXMS}.get_fee", fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf"))
|
||||
mocker.patch(
|
||||
"freqtrade.persistence.trade_model.price_to_precision", lambda p, *args, **kwargs: p
|
||||
)
|
||||
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
|
||||
mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p)
|
||||
patch_exchange(mocker)
|
||||
@@ -1842,6 +1843,7 @@ def test_backtest_multi_pair_long_short_switch(
|
||||
if use_detail:
|
||||
default_conf_usdt["timeframe_detail"] = "1m"
|
||||
|
||||
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf"))
|
||||
mocker.patch(f"{EXMS}.get_fee", fee)
|
||||
|
||||
@@ -39,13 +39,34 @@ def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_res
|
||||
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
|
||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||
correct = hl.hyperopt_loss_function(
|
||||
hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=hyperopt_results,
|
||||
trade_count=600,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
over = hl.hyperopt_loss_function(
|
||||
hyperopt_results, 600 + 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=hyperopt_results,
|
||||
trade_count=600 + 100,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
under = hl.hyperopt_loss_function(
|
||||
hyperopt_results, 600 - 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=hyperopt_results,
|
||||
trade_count=600 - 100,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
assert over > correct
|
||||
assert under > correct
|
||||
@@ -58,9 +79,25 @@ def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results)
|
||||
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
|
||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||
longer = hl.hyperopt_loss_function(
|
||||
hyperopt_results, 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=hyperopt_results,
|
||||
trade_count=100,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
shorter = hl.hyperopt_loss_function(
|
||||
results=resultsb,
|
||||
trade_count=100,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": resultsb["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
shorter = hl.hyperopt_loss_function(resultsb, 100, datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
assert shorter < longer
|
||||
|
||||
|
||||
@@ -73,11 +110,34 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
|
||||
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
|
||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||
correct = hl.hyperopt_loss_function(
|
||||
hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=hyperopt_results,
|
||||
trade_count=600,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
over = hl.hyperopt_loss_function(
|
||||
results=results_over,
|
||||
trade_count=600,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
over = hl.hyperopt_loss_function(results_over, 600, datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
under = hl.hyperopt_loss_function(
|
||||
results_under, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=results_under,
|
||||
trade_count=600,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
assert over < correct
|
||||
assert under > correct
|
||||
@@ -109,31 +169,34 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct
|
||||
default_conf.update({"hyperopt_loss": lossfunction})
|
||||
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||
correct = hl.hyperopt_loss_function(
|
||||
hyperopt_results,
|
||||
results=hyperopt_results,
|
||||
trade_count=len(hyperopt_results),
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=default_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=default_conf["dry_run_wallet"],
|
||||
)
|
||||
over = hl.hyperopt_loss_function(
|
||||
results_over,
|
||||
results=results_over,
|
||||
trade_count=len(results_over),
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=default_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
|
||||
starting_balance=default_conf["dry_run_wallet"],
|
||||
)
|
||||
under = hl.hyperopt_loss_function(
|
||||
results_under,
|
||||
results=results_under,
|
||||
trade_count=len(results_under),
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=default_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
|
||||
starting_balance=default_conf["dry_run_wallet"],
|
||||
)
|
||||
assert over < correct
|
||||
assert under > correct
|
||||
|
||||
@@ -26,8 +26,7 @@ from freqtrade.optimize.optimize_reports import (
|
||||
generate_strategy_comparison,
|
||||
generate_trading_stats,
|
||||
show_sorted_pairlist,
|
||||
store_backtest_analysis_results,
|
||||
store_backtest_stats,
|
||||
store_backtest_results,
|
||||
text_table_bt_results,
|
||||
text_table_strategy,
|
||||
)
|
||||
@@ -226,8 +225,9 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
|
||||
filename_last = tmp_path / LAST_BT_RESULT_FN
|
||||
_backup_file(filename_last, copy_file=True)
|
||||
assert not filename.is_file()
|
||||
default_conf["exportfilename"] = filename
|
||||
|
||||
store_backtest_stats(filename, stats, "2022_01_01_15_05_13")
|
||||
store_backtest_results(default_conf, stats, "2022_01_01_15_05_13")
|
||||
|
||||
# get real Filename (it's btresult-<date>.json)
|
||||
last_fn = get_latest_backtest_filename(filename_last.parent)
|
||||
@@ -246,11 +246,12 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
|
||||
filename1.unlink()
|
||||
|
||||
|
||||
def test_store_backtest_stats(testdatadir, mocker):
|
||||
def test_store_backtest_results(testdatadir, mocker):
|
||||
dump_mock = mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.file_dump_json")
|
||||
|
||||
data = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
|
||||
store_backtest_stats(testdatadir, data, "2022_01_01_15_05_13")
|
||||
|
||||
store_backtest_results({"exportfilename": testdatadir}, data, "2022_01_01_15_05_13")
|
||||
|
||||
assert dump_mock.call_count == 3
|
||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||
@@ -258,16 +259,16 @@ def test_store_backtest_stats(testdatadir, mocker):
|
||||
|
||||
dump_mock.reset_mock()
|
||||
filename = testdatadir / "testresult.json"
|
||||
store_backtest_stats(filename, data, "2022_01_01_15_05_13")
|
||||
store_backtest_results({"exportfilename": filename}, data, "2022_01_01_15_05_13")
|
||||
assert dump_mock.call_count == 3
|
||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||
# result will be testdatadir / testresult-<timestamp>.json
|
||||
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / "testresult"))
|
||||
|
||||
|
||||
def test_store_backtest_stats_real(tmp_path):
|
||||
def test_store_backtest_results_real(tmp_path):
|
||||
data = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
|
||||
store_backtest_stats(tmp_path, data, "2022_01_01_15_05_13")
|
||||
store_backtest_results({"exportfilename": tmp_path}, data, "2022_01_01_15_05_13")
|
||||
|
||||
assert (tmp_path / "backtest-result-2022_01_01_15_05_13.json").is_file()
|
||||
assert (tmp_path / "backtest-result-2022_01_01_15_05_13.meta.json").is_file()
|
||||
@@ -276,7 +277,9 @@ def test_store_backtest_stats_real(tmp_path):
|
||||
fn = get_latest_backtest_filename(tmp_path)
|
||||
assert fn == "backtest-result-2022_01_01_15_05_13.json"
|
||||
|
||||
store_backtest_stats(tmp_path, data, "2024_01_01_15_05_25", market_change_data=pd.DataFrame())
|
||||
store_backtest_results(
|
||||
{"exportfilename": tmp_path}, data, "2024_01_01_15_05_25", market_change_data=pd.DataFrame()
|
||||
)
|
||||
assert (tmp_path / "backtest-result-2024_01_01_15_05_25.json").is_file()
|
||||
assert (tmp_path / "backtest-result-2024_01_01_15_05_25.meta.json").is_file()
|
||||
assert (tmp_path / "backtest-result-2024_01_01_15_05_25_market_change.feather").is_file()
|
||||
@@ -287,13 +290,27 @@ def test_store_backtest_stats_real(tmp_path):
|
||||
assert fn == "backtest-result-2024_01_01_15_05_25.json"
|
||||
|
||||
|
||||
def test_store_backtest_candles(testdatadir, mocker):
|
||||
def test_store_backtest_candles(tmp_path, mocker):
|
||||
mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.file_dump_json")
|
||||
dump_mock = mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.file_dump_joblib")
|
||||
|
||||
candle_dict = {"DefStrat": {"UNITTEST/BTC": pd.DataFrame()}}
|
||||
bt_results = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
|
||||
|
||||
mock_conf = {
|
||||
"exportfilename": tmp_path,
|
||||
"export": "signals",
|
||||
"runmode": "backtest",
|
||||
}
|
||||
|
||||
# mock directory exporting
|
||||
store_backtest_analysis_results(testdatadir, candle_dict, {}, {}, "2022_01_01_15_05_13")
|
||||
data = {
|
||||
"signals": candle_dict,
|
||||
"rejected": {},
|
||||
"exited": {},
|
||||
}
|
||||
|
||||
store_backtest_results(mock_conf, bt_results, "2022_01_01_15_05_13", analysis_results=data)
|
||||
|
||||
assert dump_mock.call_count == 3
|
||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||
@@ -303,11 +320,12 @@ def test_store_backtest_candles(testdatadir, mocker):
|
||||
|
||||
dump_mock.reset_mock()
|
||||
# mock file exporting
|
||||
filename = Path(testdatadir / "testresult")
|
||||
store_backtest_analysis_results(filename, candle_dict, {}, {}, "2022_01_01_15_05_13")
|
||||
filename = Path(tmp_path / "testresult")
|
||||
mock_conf["exportfilename"] = filename
|
||||
store_backtest_results(mock_conf, bt_results, "2022_01_01_15_05_13", analysis_results=data)
|
||||
assert dump_mock.call_count == 3
|
||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||
# result will be testdatadir / testresult-<timestamp>_signals.pkl
|
||||
# result will be tmp_path / testresult-<timestamp>_signals.pkl
|
||||
assert str(dump_mock.call_args_list[0][0][0]).endswith("_signals.pkl")
|
||||
assert str(dump_mock.call_args_list[1][0][0]).endswith("_rejected.pkl")
|
||||
assert str(dump_mock.call_args_list[2][0][0]).endswith("_exited.pkl")
|
||||
@@ -317,10 +335,21 @@ def test_store_backtest_candles(testdatadir, mocker):
|
||||
|
||||
def test_write_read_backtest_candles(tmp_path):
|
||||
candle_dict = {"DefStrat": {"UNITTEST/BTC": pd.DataFrame()}}
|
||||
bt_results = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
|
||||
|
||||
mock_conf = {
|
||||
"exportfilename": tmp_path,
|
||||
"export": "signals",
|
||||
"runmode": "backtest",
|
||||
}
|
||||
# test directory exporting
|
||||
sample_date = "2022_01_01_15_05_13"
|
||||
store_backtest_analysis_results(tmp_path, candle_dict, {}, {}, sample_date)
|
||||
data = {
|
||||
"signals": candle_dict,
|
||||
"rejected": {},
|
||||
"exited": {},
|
||||
}
|
||||
store_backtest_results(mock_conf, bt_results, sample_date, analysis_results=data)
|
||||
stored_file = tmp_path / f"backtest-result-{sample_date}_signals.pkl"
|
||||
with stored_file.open("rb") as scp:
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
@@ -335,7 +364,8 @@ def test_write_read_backtest_candles(tmp_path):
|
||||
|
||||
# test file exporting
|
||||
filename = tmp_path / "testresult"
|
||||
store_backtest_analysis_results(filename, candle_dict, {}, {}, sample_date)
|
||||
mock_conf["exportfilename"] = filename
|
||||
store_backtest_results(mock_conf, bt_results, sample_date, analysis_results=data)
|
||||
stored_file = tmp_path / f"testresult-{sample_date}_signals.pkl"
|
||||
with stored_file.open("rb") as scp:
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
|
||||
@@ -37,6 +37,7 @@ def test_PairLocks(use_db):
|
||||
assert not PairLocks.is_pair_locked(pair)
|
||||
assert not PairLocks.is_pair_locked(pair, side="long")
|
||||
assert PairLocks.is_pair_locked(pair, side="short")
|
||||
assert len(PairLocks.get_pair_locks(pair)) == 1
|
||||
|
||||
# XRP/BTC should not be locked now
|
||||
pair = "XRP/BTC"
|
||||
@@ -47,9 +48,11 @@ def test_PairLocks(use_db):
|
||||
PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4))
|
||||
assert PairLocks.is_pair_locked(pair)
|
||||
|
||||
# Get both locks from above
|
||||
# Get all locks from above
|
||||
locks = PairLocks.get_pair_locks(None)
|
||||
assert len(locks) == 2
|
||||
assert len(locks) == 4
|
||||
|
||||
assert len(PairLocks.get_pair_locks(None, side="*")) == 2
|
||||
|
||||
# Unlock original pair
|
||||
pair = "ETH/BTC"
|
||||
|
||||
@@ -360,9 +360,16 @@ def test_gen_pairlist_from_tickers(mocker, rpl_config, tickers):
|
||||
exchange = get_patched_exchange(mocker, rpl_config, exchange="binance")
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
|
||||
remote_pairlist = PercentChangePairList(
|
||||
exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0
|
||||
)
|
||||
remote_pairlist = pairlistmanager._pairlist_handlers[0]
|
||||
|
||||
# The generator returns BTC ETH and TKN - filtering the first ensures removing pairs
|
||||
# in this step ain't problematic.
|
||||
def _validate_pair(pair, ticker):
|
||||
if pair == "BTC/USDT":
|
||||
return False
|
||||
return True
|
||||
|
||||
remote_pairlist._validate_pair = _validate_pair
|
||||
|
||||
result = remote_pairlist.gen_pairlist(tickers.return_value)
|
||||
|
||||
|
||||
+34
-6
@@ -514,8 +514,13 @@ def test_rpc_balance_handle_error(default_conf, mocker):
|
||||
patch_get_signal(freqtradebot)
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter({})
|
||||
with pytest.raises(RPCException, match="Error getting current tickers."):
|
||||
rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"])
|
||||
res = rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"])
|
||||
assert res["stake"] == "BTC"
|
||||
|
||||
assert len(res["currencies"]) == 1
|
||||
assert res["currencies"][0]["currency"] == "BTC"
|
||||
# ETH has not been converted.
|
||||
assert all(currency["currency"] != "ETH" for currency in res["currencies"])
|
||||
|
||||
|
||||
def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
|
||||
@@ -530,6 +535,13 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
|
||||
"total": 5.0,
|
||||
"used": 4.0,
|
||||
},
|
||||
# Invalid coin not in tickers list.
|
||||
# This triggers a 2nd call to get_tickers
|
||||
"NotACoin": {
|
||||
"free": 0.0,
|
||||
"total": 2.0,
|
||||
"used": 0.0,
|
||||
},
|
||||
"USDT": {
|
||||
"free": 50.0,
|
||||
"total": 100.0,
|
||||
@@ -574,7 +586,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
|
||||
fetch_positions=MagicMock(return_value=mock_pos),
|
||||
get_tickers=tickers,
|
||||
get_valid_pair_combination=MagicMock(
|
||||
side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}"
|
||||
side_effect=lambda a, b: [f"{b}/{a}" if a == "USDT" else f"{a}/{b}"]
|
||||
),
|
||||
)
|
||||
default_conf_usdt["dry_run"] = False
|
||||
@@ -590,8 +602,10 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
|
||||
|
||||
assert pytest.approx(result["total"]) == 2824.83464
|
||||
assert pytest.approx(result["value"]) == 2824.83464 * 1.2
|
||||
assert tickers.call_count == 1
|
||||
assert tickers.call_count == 4
|
||||
assert tickers.call_args_list[0][1]["cached"] is True
|
||||
# Testing futures - so we should get spot tickers
|
||||
assert tickers.call_args_list[-1][1]["market_type"] == "spot"
|
||||
assert "USD" == result["symbol"]
|
||||
assert result["currencies"] == [
|
||||
{
|
||||
@@ -622,6 +636,20 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
|
||||
"is_bot_managed": False,
|
||||
"is_position": False,
|
||||
},
|
||||
{
|
||||
"currency": "NotACoin",
|
||||
"balance": 2.0,
|
||||
"bot_owned": 0,
|
||||
"est_stake": 0,
|
||||
"est_stake_bot": 0,
|
||||
"free": 0.0,
|
||||
"is_bot_managed": False,
|
||||
"is_position": False,
|
||||
"position": 0,
|
||||
"side": "long",
|
||||
"stake": "USDT",
|
||||
"used": 0.0,
|
||||
},
|
||||
{
|
||||
"currency": "USDT",
|
||||
"free": 50.0,
|
||||
@@ -652,8 +680,8 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
|
||||
]
|
||||
assert pytest.approx(result["total_bot"]) == 69.5
|
||||
assert pytest.approx(result["total"]) == 2824.83464 # ETH stake is missing.
|
||||
assert result["starting_capital"] == 50
|
||||
assert result["starting_capital_ratio"] == pytest.approx(0.3899999)
|
||||
assert result["starting_capital"] == 50 * default_conf_usdt["tradable_balance_ratio"]
|
||||
assert result["starting_capital_ratio"] == pytest.approx(0.4040404)
|
||||
|
||||
|
||||
def test_rpc_start(mocker, default_conf) -> None:
|
||||
|
||||
@@ -556,7 +556,7 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers):
|
||||
ftbot.config["dry_run"] = False
|
||||
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
|
||||
mocker.patch(f"{EXMS}.get_tickers", tickers)
|
||||
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}")
|
||||
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"])
|
||||
ftbot.wallets.update()
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/balance")
|
||||
@@ -1056,6 +1056,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
||||
)
|
||||
def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected):
|
||||
ftbot, client = botclient
|
||||
ftbot.config["tradable_balance_ratio"] = 1
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
|
||||
@@ -960,7 +960,7 @@ async def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance
|
||||
default_conf["dry_run"] = False
|
||||
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
|
||||
mocker.patch(f"{EXMS}.get_tickers", tickers)
|
||||
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}")
|
||||
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"])
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
@@ -1049,7 +1049,7 @@ async def test_telegram_balance_handle_futures(
|
||||
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
|
||||
mocker.patch(f"{EXMS}.fetch_positions", return_value=mock_pos)
|
||||
mocker.patch(f"{EXMS}.get_tickers", tickers)
|
||||
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: f"{a}/{b}")
|
||||
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"])
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
@@ -1087,7 +1087,7 @@ async def test_balance_handle_empty_response_dry(default_conf, update, mocker) -
|
||||
result = msg_mock.call_args_list[0][0][0]
|
||||
assert msg_mock.call_count == 1
|
||||
assert "*Warning:* Simulated balances in Dry Mode." in result
|
||||
assert "Starting capital: `1000 BTC`" in result
|
||||
assert "Starting capital: `990 BTC`" in result
|
||||
|
||||
|
||||
async def test_balance_handle_too_large_response(default_conf, update, mocker) -> None:
|
||||
|
||||
@@ -1481,6 +1481,12 @@ def test_flat_vars_to_nested_dict(caplog):
|
||||
"FREQTRADE__STAKE_AMOUNT": "200.05",
|
||||
"FREQTRADE__TELEGRAM__CHAT_ID": "2151",
|
||||
"NOT_RELEVANT": "200.0", # Will be ignored
|
||||
"FREQTRADE__ARRAY": '[{"name":"default","host":"xxx"}]',
|
||||
"FREQTRADE__EXCHANGE__PAIR_WHITELIST": '["BTC/USDT", "ETH/USDT"]',
|
||||
# Fails due to trailing comma
|
||||
"FREQTRADE__ARRAY_TRAIL_COMMA": '[{"name":"default","host":"xxx",}]',
|
||||
# Object fails
|
||||
"FREQTRADE__OBJECT": '{"name":"default","host":"xxx"}',
|
||||
}
|
||||
expected = {
|
||||
"stake_amount": 200.05,
|
||||
@@ -1494,8 +1500,12 @@ def test_flat_vars_to_nested_dict(caplog):
|
||||
},
|
||||
"some_setting": True,
|
||||
"some_false_setting": False,
|
||||
"pair_whitelist": ["BTC/USDT", "ETH/USDT"],
|
||||
},
|
||||
"telegram": {"chat_id": "2151"},
|
||||
"array": [{"name": "default", "host": "xxx"}],
|
||||
"object": '{"name":"default","host":"xxx"}',
|
||||
"array_trail_comma": '[{"name":"default","host":"xxx",}]',
|
||||
}
|
||||
res = _flat_vars_to_nested_dict(test_args, ENV_VAR_PREFIX)
|
||||
assert res == expected
|
||||
|
||||
+158
-4
@@ -168,7 +168,7 @@ def test_get_trade_stake_amount_unlimited_amount(
|
||||
assert result == 0
|
||||
|
||||
freqtrade.config["dry_run_wallet"] = 200
|
||||
freqtrade.wallets._start_cap = 200
|
||||
freqtrade.wallets._start_cap["BTC"] = 200
|
||||
result = freqtrade.wallets.get_trade_stake_amount("XRP/USDT", 3)
|
||||
assert round(result, 4) == round(result2, 4)
|
||||
|
||||
@@ -244,7 +244,7 @@ def test_get_starting_balance(
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
assert freqtrade.wallets.get_starting_balance() == expected
|
||||
assert freqtrade.wallets.get_starting_balance() == expected * (1 if available_capital else 0.99)
|
||||
|
||||
|
||||
def test_sync_wallet_futures_live(mocker, default_conf):
|
||||
@@ -373,7 +373,10 @@ def test_sync_wallet_dry(mocker, default_conf_usdt, fee):
|
||||
# sum of used and free should be total.
|
||||
assert usdt_bal.total == usdt_bal.free + usdt_bal.used
|
||||
|
||||
assert freqtrade.wallets.get_starting_balance() == default_conf_usdt["dry_run_wallet"]
|
||||
assert (
|
||||
freqtrade.wallets.get_starting_balance()
|
||||
== default_conf_usdt["dry_run_wallet"] * default_conf_usdt["tradable_balance_ratio"]
|
||||
)
|
||||
total = freqtrade.wallets.get_total("LTC")
|
||||
free = freqtrade.wallets.get_free("LTC")
|
||||
used = freqtrade.wallets.get_used("LTC")
|
||||
@@ -401,7 +404,10 @@ def test_sync_wallet_futures_dry(mocker, default_conf, fee):
|
||||
assert positions["XRP/BTC"].side == "long"
|
||||
assert positions["LTC/BTC"].side == "short"
|
||||
|
||||
assert freqtrade.wallets.get_starting_balance() == default_conf["dry_run_wallet"]
|
||||
assert (
|
||||
freqtrade.wallets.get_starting_balance()
|
||||
== default_conf["dry_run_wallet"] * default_conf["tradable_balance_ratio"]
|
||||
)
|
||||
total = freqtrade.wallets.get_total("BTC")
|
||||
free = freqtrade.wallets.get_free("BTC")
|
||||
used = freqtrade.wallets.get_used("BTC")
|
||||
@@ -451,3 +457,151 @@ def test_check_exit_amount_futures(mocker, default_conf, fee):
|
||||
assert freqtrade.wallets.check_exit_amount(trade) is False
|
||||
assert total_mock.call_count == 0
|
||||
assert update_mock.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config,wallets",
|
||||
[
|
||||
(
|
||||
{"stake_currency": "USDT", "dry_run_wallet": 1000.0},
|
||||
{"USDT": {"currency": "USDT", "free": 1000.0, "used": 0.0, "total": 1000.0}},
|
||||
),
|
||||
(
|
||||
{"stake_currency": "USDT", "dry_run_wallet": {"USDT": 1000.0, "BTC": 0.1, "ETH": 2.0}},
|
||||
{
|
||||
"USDT": {"currency": "USDT", "free": 1000.0, "used": 0.0, "total": 1000.0},
|
||||
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
|
||||
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
"stake_currency": "USDT",
|
||||
"margin_mode": "cross",
|
||||
"dry_run_wallet": {"USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
|
||||
},
|
||||
{
|
||||
# USDT wallet should be created with 0 balance, but Free balance, since
|
||||
# it's converted from the other currencies
|
||||
"USDT": {"currency": "USDT", "free": 4200.0, "used": 0.0, "total": 0.0},
|
||||
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
|
||||
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
|
||||
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
"stake_currency": "USDT",
|
||||
"margin_mode": "cross",
|
||||
"dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
|
||||
},
|
||||
{
|
||||
# USDT wallet should be created with 500 balance, but Free balance, since
|
||||
# it's converted from the other currencies
|
||||
"USDT": {"currency": "USDT", "free": 4700.0, "used": 0.0, "total": 500.0},
|
||||
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
|
||||
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
|
||||
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
|
||||
},
|
||||
),
|
||||
(
|
||||
# Same as above, but without cross
|
||||
{
|
||||
"stake_currency": "USDT",
|
||||
"dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
|
||||
},
|
||||
{
|
||||
# No "free" transfer for USDT wallet
|
||||
"USDT": {"currency": "USDT", "free": 500.0, "used": 0.0, "total": 500.0},
|
||||
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
|
||||
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
|
||||
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
|
||||
},
|
||||
),
|
||||
(
|
||||
# Same as above, but with futures and cross
|
||||
{
|
||||
"stake_currency": "USDT",
|
||||
"margin_mode": "cross",
|
||||
"trading_mode": "futures",
|
||||
"dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
|
||||
},
|
||||
{
|
||||
# USDT wallet should be created with 500 balance, but Free balance, since
|
||||
# it's converted from the other currencies
|
||||
"USDT": {"currency": "USDT", "free": 4700.0, "used": 0.0, "total": 500.0},
|
||||
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
|
||||
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
|
||||
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallets):
|
||||
default_conf_usdt.update(config)
|
||||
mocker.patch(
|
||||
f"{EXMS}.get_tickers",
|
||||
return_value={
|
||||
"USDC/USDT": {"last": 1.0},
|
||||
"BTC/USDT": {"last": 20_000.0},
|
||||
"ETH/USDT": {"last": 1100.0},
|
||||
},
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
stake_currency = config["stake_currency"]
|
||||
# Verify each wallet matches the expected values
|
||||
for currency, expected_wallet in wallets.items():
|
||||
wallet = freqtrade.wallets._wallets[currency]
|
||||
assert wallet.currency == expected_wallet["currency"]
|
||||
assert wallet.free == expected_wallet["free"]
|
||||
assert wallet.used == expected_wallet["used"]
|
||||
assert wallet.total == expected_wallet["total"]
|
||||
|
||||
# Verify no extra wallets were created
|
||||
assert len(freqtrade.wallets._wallets) == len(wallets)
|
||||
|
||||
# Create a trade and verify the new currency is added to the wallets
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.0)
|
||||
mocker.patch(f"{EXMS}.get_rate", return_value=2.22)
|
||||
mocker.patch(
|
||||
f"{EXMS}.fetch_ticker",
|
||||
return_value={
|
||||
"bid": 0.20,
|
||||
"ask": 0.22,
|
||||
"last": 0.22,
|
||||
},
|
||||
)
|
||||
# Without position, collateral will be the same as free
|
||||
assert freqtrade.wallets.get_collateral() == freqtrade.wallets.get_free(stake_currency)
|
||||
freqtrade.execute_entry("NEO/USDT", 100.0)
|
||||
|
||||
# Update wallets and verify NEO is now included
|
||||
freqtrade.wallets.update()
|
||||
if default_conf_usdt["trading_mode"] != "futures":
|
||||
assert "NEO" in freqtrade.wallets._wallets
|
||||
|
||||
assert freqtrade.wallets._wallets["NEO"].total == 45.04504504 # 100 USDT / 0.22
|
||||
assert freqtrade.wallets._wallets["NEO"].used == 0.0
|
||||
assert freqtrade.wallets._wallets["NEO"].free == 45.04504504
|
||||
assert freqtrade.wallets.get_collateral() == freqtrade.wallets.get_free(stake_currency)
|
||||
# Verify USDT wallet was reduced by trade amount
|
||||
assert (
|
||||
pytest.approx(freqtrade.wallets._wallets[stake_currency].total)
|
||||
== wallets[stake_currency]["total"] - 100.0
|
||||
)
|
||||
assert len(freqtrade.wallets._wallets) == len(wallets) + 1 # Original wallets + NEO
|
||||
else:
|
||||
# Futures mode
|
||||
assert "NEO" not in freqtrade.wallets._wallets
|
||||
assert freqtrade.wallets._positions["NEO/USDT"].position == 45.04504504
|
||||
assert pytest.approx(freqtrade.wallets._positions["NEO/USDT"].collateral) == 100
|
||||
|
||||
# Verify USDT wallet's free was reduced by trade amount
|
||||
assert (
|
||||
pytest.approx(freqtrade.wallets.get_collateral())
|
||||
== freqtrade.wallets.get_free(stake_currency) + 100
|
||||
)
|
||||
assert (
|
||||
pytest.approx(freqtrade.wallets._wallets[stake_currency].free)
|
||||
== wallets[stake_currency]["free"] - 100.0
|
||||
)
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user