Merge branch 'develop' into fix/orderflow_cache
This commit is contained in:
@@ -17,8 +17,8 @@ repos:
|
|||||||
- types-cachetools==5.5.0.20240820
|
- types-cachetools==5.5.0.20240820
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.32.0.20241016
|
- types-requests==2.32.0.20241016
|
||||||
- types-tabulate==0.9.0.20240106
|
- types-tabulate==0.9.0.20241207
|
||||||
- types-python-dateutil==2.9.0.20241003
|
- types-python-dateutil==2.9.0.20241206
|
||||||
- SQLAlchemy==2.0.36
|
- SQLAlchemy==2.0.36
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.8.1'
|
rev: 'v0.8.2'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
@@ -56,6 +56,11 @@ repos:
|
|||||||
.*\.md
|
.*\.md
|
||||||
)$
|
)$
|
||||||
|
|
||||||
|
- repo: https://github.com/stefmolin/exif-stripper
|
||||||
|
rev: 0.6.1
|
||||||
|
hooks:
|
||||||
|
- id: strip-exif
|
||||||
|
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.3.0
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
@@ -102,8 +102,17 @@
|
|||||||
},
|
},
|
||||||
"dry_run_wallet": {
|
"dry_run_wallet": {
|
||||||
"description": "Initial wallet balance for dry run mode.",
|
"description": "Initial wallet balance for dry run mode.",
|
||||||
"type": "number",
|
"type": [
|
||||||
"default": 1000
|
"number",
|
||||||
|
"object"
|
||||||
|
],
|
||||||
|
"default": 1000,
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-zA-Z0-9]+$": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"cancel_open_orders_on_exit": {
|
"cancel_open_orders_on_exit": {
|
||||||
"description": "Cancel open orders when exiting.",
|
"description": "Cancel open orders when exiting.",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
|
|||||||
config: Config,
|
config: Config,
|
||||||
processed: dict[str, DataFrame],
|
processed: dict[str, DataFrame],
|
||||||
backtest_stats: dict[str, Any],
|
backtest_stats: dict[str, Any],
|
||||||
|
starting_balance: float,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> float:
|
) -> 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).
|
* `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.
|
* `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`.
|
* `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.
|
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.
|
||||||
|
|
||||||
|
|||||||
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 |
+28
-3
@@ -39,13 +39,19 @@ Please note that Environment variables will overwrite corresponding settings in
|
|||||||
|
|
||||||
Common example:
|
Common example:
|
||||||
|
|
||||||
```
|
``` bash
|
||||||
FREQTRADE__TELEGRAM__CHAT_ID=<telegramchatid>
|
FREQTRADE__TELEGRAM__CHAT_ID=<telegramchatid>
|
||||||
FREQTRADE__TELEGRAM__TOKEN=<telegramToken>
|
FREQTRADE__TELEGRAM__TOKEN=<telegramToken>
|
||||||
FREQTRADE__EXCHANGE__KEY=<yourExchangeKey>
|
FREQTRADE__EXCHANGE__KEY=<yourExchangeKey>
|
||||||
FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
|
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
|
!!! 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.
|
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"
|
??? 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.
|
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
|
### 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
|
| `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
|
| `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` | **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
|
| `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
|
| `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
|
| `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
|
||||||
@@ -324,6 +330,25 @@ To limit this calculation in case of large stoploss values, the calculated minim
|
|||||||
!!! Warning
|
!!! 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.
|
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
|
#### 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.
|
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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
markdown==3.7
|
markdown==3.7
|
||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-material==9.5.47
|
mkdocs-material==9.5.48
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.12
|
pymdown-extensions==10.12
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[
|
|||||||
:return: Configuration
|
:return: Configuration
|
||||||
"""
|
"""
|
||||||
from freqtrade.configuration import setup_utils_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)
|
config = setup_utils_configuration(args, method)
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[
|
|||||||
RunMode.HYPEROPT: "hyperoptimization",
|
RunMode.HYPEROPT: "hyperoptimization",
|
||||||
}
|
}
|
||||||
if method in no_unlimited_runmodes.keys():
|
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
|
# tradable_balance_ratio
|
||||||
if (
|
if (
|
||||||
config["stake_amount"] != constants.UNLIMITED_STAKE_AMOUNT
|
config["stake_amount"] != constants.UNLIMITED_STAKE_AMOUNT
|
||||||
|
|||||||
@@ -85,8 +85,10 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
"dry_run_wallet": {
|
"dry_run_wallet": {
|
||||||
"description": "Initial wallet balance for dry run mode.",
|
"description": "Initial wallet balance for dry run mode.",
|
||||||
"type": "number",
|
"type": ["number", "object"],
|
||||||
"default": DRY_RUN_WALLET,
|
"default": DRY_RUN_WALLET,
|
||||||
|
"patternProperties": {r"^[a-zA-Z0-9]+$": {"type": "number"}},
|
||||||
|
"additionalProperties": False,
|
||||||
},
|
},
|
||||||
"cancel_open_orders_on_exit": {
|
"cancel_open_orders_on_exit": {
|
||||||
"description": "Cancel open orders when exiting.",
|
"description": "Cancel open orders when exiting.",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import rapidjson
|
||||||
|
|
||||||
from freqtrade.constants import ENV_VAR_PREFIX
|
from freqtrade.constants import ENV_VAR_PREFIX
|
||||||
from freqtrade.misc import deep_merge_dicts
|
from freqtrade.misc import deep_merge_dicts
|
||||||
|
|
||||||
@@ -20,6 +22,14 @@ def _get_var_typed(val):
|
|||||||
return True
|
return True
|
||||||
elif val.lower() in ("f", "false"):
|
elif val.lower() in ("f", "false"):
|
||||||
return 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
|
# keep as string
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ def populate_dataframe_with_trades(
|
|||||||
trades = trades.loc[trades["candle_start"] >= start_date]
|
trades = trades.loc[trades["candle_start"] >= start_date]
|
||||||
trades.reset_index(inplace=True, drop=True)
|
trades.reset_index(inplace=True, drop=True)
|
||||||
|
|
||||||
|
# group trades by candle start
|
||||||
trades_grouped_by_candle_start = trades.groupby("candle_start", group_keys=False)
|
trades_grouped_by_candle_start = trades.groupby("candle_start", group_keys=False)
|
||||||
|
|
||||||
candle_start: datetime
|
candle_start: datetime
|
||||||
|
|||||||
@@ -57,8 +57,14 @@ class Binance(Exchange):
|
|||||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers:
|
def get_tickers(
|
||||||
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
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:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
# Binance's future result has no bid/ask values.
|
# Binance's future result has no bid/ask values.
|
||||||
# Therefore we must fetch that from fetch_bids_asks and combine the two results.
|
# Therefore we must fetch that from fetch_bids_asks and combine the two results.
|
||||||
@@ -85,7 +91,10 @@ class Binance(Exchange):
|
|||||||
"\nHedge Mode is not supported by freqtrade. "
|
"\nHedge Mode is not supported by freqtrade. "
|
||||||
"Please change 'Position Mode' on your binance futures account."
|
"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 += (
|
msg += (
|
||||||
"\nMulti-Asset Mode is not supported by freqtrade. "
|
"\nMulti-Asset Mode is not supported by freqtrade. "
|
||||||
"Please change 'Asset Mode' on your binance futures account."
|
"Please change 'Asset Mode' on your binance futures account."
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import asyncio
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import signal
|
import signal
|
||||||
from collections.abc import Coroutine
|
from collections.abc import Coroutine, Generator
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import floor, isnan
|
from math import floor, isnan
|
||||||
@@ -201,7 +201,7 @@ class Exchange:
|
|||||||
|
|
||||||
self._cache_lock = Lock()
|
self._cache_lock = Lock()
|
||||||
# Cache for 10 minutes ...
|
# 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
|
# 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
|
# Caching only applies to RPC methods, so prices for open trades are still
|
||||||
# refreshed once every iteration.
|
# refreshed once every iteration.
|
||||||
@@ -705,13 +705,21 @@ class Exchange:
|
|||||||
f"Available currencies are: {', '.join(quote_currencies)}"
|
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.
|
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"):
|
if pair in self.markets and self.markets[pair].get("active"):
|
||||||
return pair
|
yielded = True
|
||||||
|
yield pair
|
||||||
|
if not yielded:
|
||||||
raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
||||||
|
|
||||||
def validate_timeframes(self, timeframe: str | None) -> None:
|
def validate_timeframes(self, timeframe: str | None) -> None:
|
||||||
@@ -1801,24 +1809,37 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@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 symbols: List of symbols to fetch
|
||||||
:param cached: Allow cached result
|
:param cached: Allow cached result
|
||||||
|
:param market_type: Market type to fetch - either spot or futures.
|
||||||
:return: fetch_tickers result
|
:return: fetch_tickers result
|
||||||
"""
|
"""
|
||||||
tickers: Tickers
|
tickers: Tickers
|
||||||
if not self.exchange_has("fetchTickers"):
|
if not self.exchange_has("fetchTickers"):
|
||||||
return {}
|
return {}
|
||||||
|
cache_key = f"fetch_tickers_{market_type}" if market_type else "fetch_tickers"
|
||||||
if cached:
|
if cached:
|
||||||
with self._cache_lock:
|
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:
|
if tickers:
|
||||||
return tickers
|
return tickers
|
||||||
try:
|
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:
|
with self._cache_lock:
|
||||||
self._fetch_tickers_cache["fetch_tickers"] = tickers
|
self._fetch_tickers_cache[cache_key] = tickers
|
||||||
return tickers
|
return tickers
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
@@ -1842,7 +1863,39 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from 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
|
@retrier
|
||||||
def fetch_ticker(self, pair: str) -> Ticker:
|
def fetch_ticker(self, pair: str) -> Ticker:
|
||||||
@@ -2198,10 +2251,13 @@ class Exchange:
|
|||||||
# If cost is None or 0.0 -> falsy, return None
|
# If cost is None or 0.0 -> falsy, return None
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
comb = self.get_valid_pair_combination(fee_curr, self._config["stake_currency"])
|
for comb in self.get_valid_pair_combination(
|
||||||
|
fee_curr, self._config["stake_currency"]
|
||||||
|
):
|
||||||
tick = self.fetch_ticker(comb)
|
tick = self.fetch_ticker(comb)
|
||||||
|
|
||||||
fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask")
|
fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask")
|
||||||
|
if tick:
|
||||||
|
break
|
||||||
except (ValueError, ExchangeError):
|
except (ValueError, ExchangeError):
|
||||||
fee_to_quote_rate = self._config["exchange"].get("unknown_fee_rate", None)
|
fee_to_quote_rate = self._config["exchange"].get("unknown_fee_rate", None)
|
||||||
if not fee_to_quote_rate:
|
if not fee_to_quote_rate:
|
||||||
@@ -3586,7 +3642,7 @@ class Exchange:
|
|||||||
liquidation_price_buffer = (
|
liquidation_price_buffer = (
|
||||||
liquidation_price - buffer_amount if is_short else liquidation_price + buffer_amount
|
liquidation_price - buffer_amount if is_short else liquidation_price + buffer_amount
|
||||||
)
|
)
|
||||||
return max(liquidation_price_buffer, 0.0)
|
return self.price_to_precision(pair, max(liquidation_price_buffer, 0.0))
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -308,7 +308,9 @@ def price_to_precision(
|
|||||||
decimal_to_precision(
|
decimal_to_precision(
|
||||||
price,
|
price,
|
||||||
rounding_mode=rounding_mode,
|
rounding_mode=rounding_mode,
|
||||||
precision=price_precision,
|
precision=int(price_precision)
|
||||||
|
if precisionMode != TICK_SIZE
|
||||||
|
else price_precision,
|
||||||
counting_mode=precisionMode,
|
counting_mode=precisionMode,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,11 +50,17 @@ class Kraken(Exchange):
|
|||||||
|
|
||||||
return parent_check and market.get("darkpool", False) is False
|
return parent_check and market.get("darkpool", False) is False
|
||||||
|
|
||||||
def get_tickers(self, symbols: 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
|
# Only fetch tickers for current stake currency
|
||||||
# Otherwise the request for kraken becomes too large.
|
# Otherwise the request for kraken becomes too large.
|
||||||
symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]]))
|
symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]]))
|
||||||
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:
|
def consolidate_balances(self, balances: CcxtBalances) -> CcxtBalances:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from xgboost import XGBRFRegressor
|
|||||||
|
|
||||||
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
from freqtrade.freqai.tensorboard import TBCallback
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -45,7 +44,12 @@ class XGBoostRFRegressor(BaseRegressionModel):
|
|||||||
|
|
||||||
model = XGBRFRegressor(**self.model_training_parameters)
|
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(
|
model.fit(
|
||||||
X=X,
|
X=X,
|
||||||
y=y,
|
y=y,
|
||||||
@@ -55,6 +59,6 @@ class XGBoostRFRegressor(BaseRegressionModel):
|
|||||||
xgb_model=xgb_model,
|
xgb_model=xgb_model,
|
||||||
)
|
)
|
||||||
# set the callbacks to empty so that we can serialize to disk later
|
# set the callbacks to empty so that we can serialize to disk later
|
||||||
model.set_params(callbacks=[])
|
# model.set_params(callbacks=[])
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ def update_liquidation_prices(
|
|||||||
total_wallet_stake = 0.0
|
total_wallet_stake = 0.0
|
||||||
if dry_run:
|
if dry_run:
|
||||||
# Parameters only needed for cross margin
|
# 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.")
|
logger.info(
|
||||||
|
"Updating liquidation price for all open trades. "
|
||||||
|
f"Collateral {total_wallet_stake} {stake_currency}."
|
||||||
|
)
|
||||||
open_trades = Trade.get_open_trades()
|
open_trades = Trade.get_open_trades()
|
||||||
for t in open_trades:
|
for t in open_trades:
|
||||||
# TODO: This should be done in a batch update
|
# TODO: This should be done in a batch update
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from freqtrade.constants import Config
|
|||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.optimize.analysis.lookahead import LookaheadAnalysis
|
from freqtrade.optimize.analysis.lookahead import LookaheadAnalysis
|
||||||
from freqtrade.resolvers import StrategyResolver
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -163,7 +163,7 @@ class LookaheadAnalysisSubFunctions:
|
|||||||
config["max_open_trades"] = len(config["pairs"])
|
config["max_open_trades"] = len(config["pairs"])
|
||||||
|
|
||||||
min_dry_run_wallet = 1000000000
|
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(
|
logger.info(
|
||||||
"Dry run wallet was not set to 1 billion, pushing it up there "
|
"Dry run wallet was not set to 1 billion, pushing it up there "
|
||||||
"just to avoid false positives"
|
"just to avoid false positives"
|
||||||
|
|||||||
@@ -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.hyperopt_tools import HyperoptStateContainer, HyperoptTools
|
||||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||||
|
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
|
||||||
|
|
||||||
|
|
||||||
# Suppress scikit-learn FutureWarnings from skopt
|
# Suppress scikit-learn FutureWarnings from skopt
|
||||||
@@ -363,6 +364,7 @@ class HyperOptimizer:
|
|||||||
config=self.config,
|
config=self.config,
|
||||||
processed=processed,
|
processed=processed,
|
||||||
backtest_stats=strat_stats,
|
backtest_stats=strat_stats,
|
||||||
|
starting_balance=get_dry_run_wallet(self.config),
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"loss": loss,
|
"loss": loss,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
|||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.data.metrics import calculate_calmar
|
from freqtrade.data.metrics import calculate_calmar
|
||||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||||
|
|
||||||
@@ -24,10 +23,9 @@ class CalmarHyperOptLoss(IHyperOptLoss):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def hyperopt_loss_function(
|
def hyperopt_loss_function(
|
||||||
results: DataFrame,
|
results: DataFrame,
|
||||||
trade_count: int,
|
|
||||||
min_date: datetime,
|
min_date: datetime,
|
||||||
max_date: datetime,
|
max_date: datetime,
|
||||||
config: Config,
|
starting_balance: float,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> float:
|
) -> float:
|
||||||
@@ -36,7 +34,6 @@ class CalmarHyperOptLoss(IHyperOptLoss):
|
|||||||
|
|
||||||
Uses Calmar Ratio calculation.
|
Uses Calmar Ratio calculation.
|
||||||
"""
|
"""
|
||||||
starting_balance = config["dry_run_wallet"]
|
|
||||||
calmar_ratio = calculate_calmar(results, min_date, max_date, starting_balance)
|
calmar_ratio = calculate_calmar(results, min_date, max_date, starting_balance)
|
||||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||||
return -calmar_ratio
|
return -calmar_ratio
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class IHyperOptLoss(ABC):
|
|||||||
config: Config,
|
config: Config,
|
||||||
processed: dict[str, DataFrame],
|
processed: dict[str, DataFrame],
|
||||||
backtest_stats: dict[str, Any],
|
backtest_stats: dict[str, Any],
|
||||||
|
starting_balance: float,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ Hyperoptimization.
|
|||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.data.metrics import calculate_underwater
|
from freqtrade.data.metrics import calculate_underwater
|
||||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||||
|
|
||||||
@@ -21,7 +20,9 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
Objective function.
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss):
|
|||||||
total_profit = results["profit_abs"].sum()
|
total_profit = results["profit_abs"].sum()
|
||||||
try:
|
try:
|
||||||
drawdown_df = calculate_underwater(
|
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"]))
|
max_drawdown = abs(min(drawdown_df["drawdown"]))
|
||||||
relative_drawdown = max(drawdown_df["drawdown_relative"])
|
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
|
import numpy as np
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
|
from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
|
||||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss):
|
|||||||
def hyperopt_loss_function(
|
def hyperopt_loss_function(
|
||||||
results: DataFrame,
|
results: DataFrame,
|
||||||
trade_count: int,
|
trade_count: int,
|
||||||
config: Config,
|
starting_balance: float,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> float:
|
) -> float:
|
||||||
total_profit = results["profit_abs"].sum()
|
total_profit = results["profit_abs"].sum()
|
||||||
@@ -83,7 +82,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss):
|
|||||||
# Calculate drawdown
|
# Calculate drawdown
|
||||||
try:
|
try:
|
||||||
drawdown = calculate_max_drawdown(
|
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
|
relative_account_drawdown = drawdown.relative_account_drawdown
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ individual needs.
|
|||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.data.metrics import calculate_max_drawdown
|
from freqtrade.data.metrics import calculate_max_drawdown
|
||||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||||
|
|
||||||
@@ -21,12 +20,14 @@ DRAWDOWN_MULT = 0.075
|
|||||||
|
|
||||||
class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
|
class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
|
||||||
@staticmethod
|
@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()
|
total_profit = results["profit_abs"].sum()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
drawdown = calculate_max_drawdown(
|
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
|
relative_account_drawdown = drawdown.relative_account_drawdown
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
|||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.data.metrics import calculate_sharpe
|
from freqtrade.data.metrics import calculate_sharpe
|
||||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||||
|
|
||||||
@@ -24,10 +23,9 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def hyperopt_loss_function(
|
def hyperopt_loss_function(
|
||||||
results: DataFrame,
|
results: DataFrame,
|
||||||
trade_count: int,
|
|
||||||
min_date: datetime,
|
min_date: datetime,
|
||||||
max_date: datetime,
|
max_date: datetime,
|
||||||
config: Config,
|
starting_balance: float,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> float:
|
) -> float:
|
||||||
@@ -36,7 +34,6 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
|||||||
|
|
||||||
Uses Sharpe Ratio calculation.
|
Uses Sharpe Ratio calculation.
|
||||||
"""
|
"""
|
||||||
starting_balance = config["dry_run_wallet"]
|
|
||||||
sharp_ratio = calculate_sharpe(results, min_date, max_date, starting_balance)
|
sharp_ratio = calculate_sharpe(results, min_date, max_date, starting_balance)
|
||||||
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||||
return -sharp_ratio
|
return -sharp_ratio
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
|||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.data.metrics import calculate_sortino
|
from freqtrade.data.metrics import calculate_sortino
|
||||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||||
|
|
||||||
@@ -24,10 +23,9 @@ class SortinoHyperOptLoss(IHyperOptLoss):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def hyperopt_loss_function(
|
def hyperopt_loss_function(
|
||||||
results: DataFrame,
|
results: DataFrame,
|
||||||
trade_count: int,
|
|
||||||
min_date: datetime,
|
min_date: datetime,
|
||||||
max_date: datetime,
|
max_date: datetime,
|
||||||
config: Config,
|
starting_balance: float,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> float:
|
) -> float:
|
||||||
@@ -36,7 +34,6 @@ class SortinoHyperOptLoss(IHyperOptLoss):
|
|||||||
|
|
||||||
Uses Sortino Ratio calculation.
|
Uses Sortino Ratio calculation.
|
||||||
"""
|
"""
|
||||||
starting_balance = config["dry_run_wallet"]
|
|
||||||
sortino_ratio = calculate_sortino(results, min_date, max_date, starting_balance)
|
sortino_ratio = calculate_sortino(results, min_date, max_date, starting_balance)
|
||||||
# print(expected_returns_mean, down_stdev, sortino_ratio)
|
# print(expected_returns_mean, down_stdev, sortino_ratio)
|
||||||
return -sortino_ratio
|
return -sortino_ratio
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from freqtrade.data.metrics import (
|
|||||||
calculate_sortino,
|
calculate_sortino,
|
||||||
)
|
)
|
||||||
from freqtrade.ft_types import BacktestResultType
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -69,7 +69,7 @@ def generate_rejected_signals(
|
|||||||
|
|
||||||
|
|
||||||
def _generate_result_line(
|
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:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Generate one result dict, with "first_column" as key.
|
Generate one result dict, with "first_column" as key.
|
||||||
@@ -111,7 +111,7 @@ def _generate_result_line(
|
|||||||
def generate_pair_metrics(
|
def generate_pair_metrics(
|
||||||
pairlist: list[str],
|
pairlist: list[str],
|
||||||
stake_currency: str,
|
stake_currency: str,
|
||||||
starting_balance: int,
|
starting_balance: float,
|
||||||
results: DataFrame,
|
results: DataFrame,
|
||||||
skip_nan: bool = False,
|
skip_nan: bool = False,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
@@ -144,7 +144,7 @@ def generate_pair_metrics(
|
|||||||
|
|
||||||
def generate_tag_metrics(
|
def generate_tag_metrics(
|
||||||
tag_type: Literal["enter_tag", "exit_reason"] | list[Literal["enter_tag", "exit_reason"]],
|
tag_type: Literal["enter_tag", "exit_reason"] | list[Literal["enter_tag", "exit_reason"]],
|
||||||
starting_balance: int,
|
starting_balance: float,
|
||||||
results: DataFrame,
|
results: DataFrame,
|
||||||
skip_nan: bool = False,
|
skip_nan: bool = False,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
@@ -373,7 +373,7 @@ def generate_strategy_stats(
|
|||||||
return {}
|
return {}
|
||||||
config = content["config"]
|
config = content["config"]
|
||||||
max_open_trades = min(config["max_open_trades"], len(pairlist))
|
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"]
|
stake_currency = config["stake_currency"]
|
||||||
|
|
||||||
pair_results = generate_pair_metrics(
|
pair_results = generate_pair_metrics(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
|||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.strategy import IStrategy
|
from freqtrade.strategy import IStrategy
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
|
from freqtrade.util import get_dry_run_wallet
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -706,7 +707,7 @@ def plot_profit(config: Config) -> None:
|
|||||||
trades,
|
trades,
|
||||||
config["timeframe"],
|
config["timeframe"],
|
||||||
config.get("stake_currency", ""),
|
config.get("stake_currency", ""),
|
||||||
config.get("available_capital", config["dry_run_wallet"]),
|
config.get("available_capital", get_dry_run_wallet(config)),
|
||||||
)
|
)
|
||||||
store_plot_file(
|
store_plot_file(
|
||||||
fig,
|
fig,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ defined period or as coming from ticker
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import TypedDict
|
||||||
|
|
||||||
from cachetools import TTLCache
|
from cachetools import TTLCache
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@@ -24,6 +24,11 @@ from freqtrade.util import dt_now, format_ms_time
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SymbolWithPercentage(TypedDict):
|
||||||
|
symbol: str
|
||||||
|
percentage: float | None
|
||||||
|
|
||||||
|
|
||||||
class PercentChangePairList(IPairList):
|
class PercentChangePairList(IPairList):
|
||||||
is_pairlist_generator = True
|
is_pairlist_generator = True
|
||||||
supports_backtesting = SupportsBacktesting.NO
|
supports_backtesting = SupportsBacktesting.NO
|
||||||
@@ -191,7 +196,6 @@ class PercentChangePairList(IPairList):
|
|||||||
for k, v in tickers.items()
|
for k, v in tickers.items()
|
||||||
if (
|
if (
|
||||||
self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
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
|
and v["symbol"] in _pairlist
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -212,13 +216,15 @@ class PercentChangePairList(IPairList):
|
|||||||
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
:param tickers: Tickers (from exchange.get_tickers). May be cached.
|
||||||
:return: new whitelist
|
: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:
|
if self._use_range:
|
||||||
# calculating using lookback_period
|
# 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:
|
else:
|
||||||
# Fetching 24h change by default from supported exchange tickers
|
# 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:
|
if self._min_value is not None:
|
||||||
filtered_tickers = [v for v in filtered_tickers if v["percentage"] > self._min_value]
|
filtered_tickers = [v for v in filtered_tickers if v["percentage"] > self._min_value]
|
||||||
@@ -228,7 +234,7 @@ class PercentChangePairList(IPairList):
|
|||||||
sorted_tickers = sorted(
|
sorted_tickers = sorted(
|
||||||
filtered_tickers,
|
filtered_tickers,
|
||||||
reverse=self._sort_direction == "desc",
|
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
|
# Validate whitelist to only have active market pairs
|
||||||
@@ -240,7 +246,7 @@ class PercentChangePairList(IPairList):
|
|||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
def fetch_candles_for_lookback_period(
|
def fetch_candles_for_lookback_period(
|
||||||
self, filtered_tickers: list[dict[str, str]]
|
self, filtered_tickers: list[SymbolWithPercentage]
|
||||||
) -> dict[PairWithTimeframe, DataFrame]:
|
) -> dict[PairWithTimeframe, DataFrame]:
|
||||||
since_ms = (
|
since_ms = (
|
||||||
int(
|
int(
|
||||||
@@ -262,7 +268,6 @@ class PercentChangePairList(IPairList):
|
|||||||
)
|
)
|
||||||
* 1000
|
* 1000
|
||||||
)
|
)
|
||||||
# todo: utc date output for starting date
|
|
||||||
self.log_once(
|
self.log_once(
|
||||||
f"Using change range of {self._lookback_period} candles, timeframe: "
|
f"Using change range of {self._lookback_period} candles, timeframe: "
|
||||||
f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} "
|
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)
|
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms)
|
||||||
return candles
|
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
|
# get lookback period in ms, for exchange ohlcv fetch
|
||||||
candles = self.fetch_candles_for_lookback_period(filtered_tickers)
|
candles = self.fetch_candles_for_lookback_period(filtered_tickers)
|
||||||
|
|
||||||
@@ -302,16 +309,23 @@ class PercentChangePairList(IPairList):
|
|||||||
filtered_tickers[i]["percentage"] = pct_change
|
filtered_tickers[i]["percentage"] = pct_change
|
||||||
else:
|
else:
|
||||||
filtered_tickers[i]["percentage"] = 0
|
filtered_tickers[i]["percentage"] = 0
|
||||||
|
return filtered_tickers
|
||||||
|
|
||||||
def fetch_percent_change_from_tickers(self, filtered_tickers: list[dict[str, Any]], tickers):
|
def fetch_percent_change_from_tickers(
|
||||||
for i, p in enumerate(filtered_tickers):
|
self, filtered_tickers: list[SymbolWithPercentage], tickers
|
||||||
|
) -> list[SymbolWithPercentage]:
|
||||||
|
valid_tickers: list[SymbolWithPercentage] = []
|
||||||
|
for p in filtered_tickers:
|
||||||
# Filter out assets
|
# Filter out assets
|
||||||
if not self._validate_pair(
|
if (
|
||||||
|
self._validate_pair(
|
||||||
p["symbol"], tickers[p["symbol"]] if p["symbol"] in tickers else None
|
p["symbol"], tickers[p["symbol"]] if p["symbol"] in tickers else None
|
||||||
|
)
|
||||||
|
and p["symbol"] != "UNI/USDT"
|
||||||
):
|
):
|
||||||
filtered_tickers.remove(p)
|
p["percentage"] = tickers[p["symbol"]]["percentage"]
|
||||||
else:
|
valid_tickers.append(p)
|
||||||
filtered_tickers[i]["percentage"] = tickers[p["symbol"]]["percentage"]
|
return valid_tickers
|
||||||
|
|
||||||
def _validate_pair(self, pair: str, ticker: Ticker | None) -> bool:
|
def _validate_pair(self, pair: str, ticker: Ticker | None) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
+18
-14
@@ -7,7 +7,7 @@ from abc import abstractmethod
|
|||||||
from collections.abc import Generator, Sequence
|
from collections.abc import Generator, Sequence
|
||||||
from datetime import date, datetime, timedelta, timezone
|
from datetime import date, datetime, timedelta, timezone
|
||||||
from math import isnan
|
from math import isnan
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
@@ -32,7 +32,7 @@ from freqtrade.enums import (
|
|||||||
)
|
)
|
||||||
from freqtrade.exceptions import ExchangeError, PricingError
|
from freqtrade.exceptions import ExchangeError, PricingError
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
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.loggers import bufferHandler
|
||||||
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
|
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
|
||||||
from freqtrade.persistence.models import PairLock
|
from freqtrade.persistence.models import PairLock
|
||||||
@@ -243,7 +243,11 @@ class RPC:
|
|||||||
stoploss_entry_dist_ratio = stop_entry.profit_ratio
|
stoploss_entry_dist_ratio = stop_entry.profit_ratio
|
||||||
|
|
||||||
# calculate distance to stoploss
|
# 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
|
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
|
||||||
|
|
||||||
trade_dict = trade.to_json()
|
trade_dict = trade.to_json()
|
||||||
@@ -670,7 +674,7 @@ class RPC:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __balance_get_est_stake(
|
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]:
|
) -> tuple[float, float]:
|
||||||
est_stake = 0.0
|
est_stake = 0.0
|
||||||
est_bot_stake = 0.0
|
est_bot_stake = 0.0
|
||||||
@@ -681,25 +685,25 @@ class RPC:
|
|||||||
est_stake = balance.free
|
est_stake = balance.free
|
||||||
est_bot_stake = amount
|
est_bot_stake = amount
|
||||||
else:
|
else:
|
||||||
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
try:
|
||||||
rate: float | None = cast(Ticker, tickers.get(pair, {})).get("last", None)
|
rate: float | None = self._freqtrade.exchange.get_conversion_rate(
|
||||||
|
coin, stake_currency
|
||||||
|
)
|
||||||
if rate:
|
if rate:
|
||||||
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
|
||||||
rate = 1.0 / rate
|
|
||||||
est_stake = rate * balance.total
|
est_stake = rate * balance.total
|
||||||
est_bot_stake = rate * amount
|
est_bot_stake = rate * amount
|
||||||
|
|
||||||
return est_stake, est_bot_stake
|
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:
|
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> dict:
|
||||||
"""Returns current account balance per crypto"""
|
"""Returns current account balance per crypto"""
|
||||||
currencies: list[dict] = []
|
currencies: list[dict] = []
|
||||||
total = 0.0
|
total = 0.0
|
||||||
total_bot = 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_trades: list[Trade] = Trade.get_open_trades()
|
||||||
open_assets: dict[str, Trade] = {t.safe_base_currency: t for t in open_trades}
|
open_assets: dict[str, Trade] = {t.safe_base_currency: t for t in open_trades}
|
||||||
@@ -715,7 +719,7 @@ class RPC:
|
|||||||
coin: str
|
coin: str
|
||||||
balance: Wallet
|
balance: Wallet
|
||||||
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
||||||
if not balance.total:
|
if not balance.total and not balance.free:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
trade = open_assets.get(coin, None)
|
trade = open_assets.get(coin, None)
|
||||||
@@ -726,7 +730,7 @@ class RPC:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
est_stake, est_stake_bot = self.__balance_get_est_stake(
|
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:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1160,10 +1160,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
logger.warning(f"Empty candle (OHLCV) data for pair {pair}")
|
logger.warning(f"Empty candle (OHLCV) data for pair {pair}")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
latest_date = dataframe["date"].max()
|
latest_date_pd = dataframe["date"].max()
|
||||||
latest = dataframe.loc[dataframe["date"] == latest_date].iloc[-1]
|
latest = dataframe.loc[dataframe["date"] == latest_date_pd].iloc[-1]
|
||||||
# Explicitly convert to datetime object to ensure the below comparison does not fail
|
# 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
|
# Check if dataframe is out of date
|
||||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from freqtrade.util.datetime_helpers import (
|
|||||||
format_ms_time,
|
format_ms_time,
|
||||||
shorten_date,
|
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.formatters import decimals_per_coin, fmt_coin, fmt_coin2, round_value
|
||||||
from freqtrade.util.ft_precise import FtPrecise
|
from freqtrade.util.ft_precise import FtPrecise
|
||||||
from freqtrade.util.measure_time import MeasureTime
|
from freqtrade.util.measure_time import MeasureTime
|
||||||
@@ -35,6 +36,7 @@ __all__ = [
|
|||||||
"dt_utc",
|
"dt_utc",
|
||||||
"format_date",
|
"format_date",
|
||||||
"format_ms_time",
|
"format_ms_time",
|
||||||
|
"get_dry_run_wallet",
|
||||||
"FtPrecise",
|
"FtPrecise",
|
||||||
"PeriodicCache",
|
"PeriodicCache",
|
||||||
"shorten_date",
|
"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")
|
||||||
+62
-28
@@ -41,7 +41,14 @@ class Wallets:
|
|||||||
self._exchange = exchange
|
self._exchange = exchange
|
||||||
self._wallets: dict[str, Wallet] = {}
|
self._wallets: dict[str, Wallet] = {}
|
||||||
self._positions: dict[str, PositionWallet] = {}
|
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._last_wallet_refresh: datetime | None = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@@ -66,6 +73,18 @@ class Wallets:
|
|||||||
else:
|
else:
|
||||||
return 0
|
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:
|
def get_owned(self, pair: str, base_currency: str) -> float:
|
||||||
"""
|
"""
|
||||||
Get currently owned value.
|
Get currently owned value.
|
||||||
@@ -109,36 +128,51 @@ class Wallets:
|
|||||||
for o in trade.open_orders
|
for o in trade.open_orders
|
||||||
if o.amount and o.ft_order_side == trade.exit_side
|
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)
|
_wallets[curr] = Wallet(
|
||||||
|
curr,
|
||||||
current_stake = self._start_cap + tot_profit - tot_in_trades
|
curr_wallet_bal + trade.amount - pending,
|
||||||
total_stake = current_stake + used_stake
|
pending,
|
||||||
|
trade.amount + curr_wallet_bal,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
tot_in_trades = 0
|
|
||||||
for position in open_trades:
|
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(
|
_positions[position.pair] = PositionWallet(
|
||||||
position.pair,
|
position.pair,
|
||||||
position=size,
|
position=position.amount,
|
||||||
leverage=leverage,
|
leverage=position.leverage,
|
||||||
collateral=collateral,
|
collateral=position.stake_amount,
|
||||||
side=position.trade_direction,
|
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(
|
used_stake = tot_in_trades
|
||||||
currency=self._config["stake_currency"],
|
|
||||||
free=current_stake,
|
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,
|
used=used_stake,
|
||||||
total=total_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._wallets = _wallets
|
||||||
self._positions = _positions
|
self._positions = _positions
|
||||||
|
|
||||||
@@ -244,7 +278,7 @@ class Wallets:
|
|||||||
else:
|
else:
|
||||||
tot_profit = Trade.get_total_closed_profit()
|
tot_profit = Trade.get_total_closed_profit()
|
||||||
open_stakes = Trade.total_open_trades_stakes()
|
open_stakes = Trade.total_open_trades_stakes()
|
||||||
available_balance = self.get_free(self._config["stake_currency"])
|
available_balance = self.get_free(self._stake_currency)
|
||||||
return available_balance - tot_profit + open_stakes
|
return available_balance - tot_profit + open_stakes
|
||||||
|
|
||||||
def get_total_stake_amount(self):
|
def get_total_stake_amount(self):
|
||||||
@@ -264,9 +298,9 @@ class Wallets:
|
|||||||
# Ensure <tradable_balance_ratio>% is used from the overall balance
|
# Ensure <tradable_balance_ratio>% is used from the overall balance
|
||||||
# Otherwise we'd risk lowering stakes with each open trade.
|
# Otherwise we'd risk lowering stakes with each open trade.
|
||||||
# (tied up + current free) * ratio) - tied up
|
# (tied up + current free) * ratio) - tied up
|
||||||
available_amount = (
|
available_amount = (val_tied_up + self.get_free(self._stake_currency)) * self._config[
|
||||||
val_tied_up + self.get_free(self._config["stake_currency"])
|
"tradable_balance_ratio"
|
||||||
) * self._config["tradable_balance_ratio"]
|
]
|
||||||
return available_amount
|
return available_amount
|
||||||
|
|
||||||
def get_available_stake_amount(self) -> float:
|
def get_available_stake_amount(self) -> float:
|
||||||
@@ -277,7 +311,7 @@ class Wallets:
|
|||||||
(<open_trade stakes> + free amount) * tradable_balance_ratio - <open_trade stakes>
|
(<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)
|
return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free)
|
||||||
|
|
||||||
def _calculate_unlimited_stake_amount(
|
def _calculate_unlimited_stake_amount(
|
||||||
@@ -316,7 +350,7 @@ class Wallets:
|
|||||||
f"lower than stake amount ({stake_amount} {self._config['stake_currency']})"
|
f"lower than stake amount ({stake_amount} {self._config['stake_currency']})"
|
||||||
)
|
)
|
||||||
|
|
||||||
return stake_amount
|
return max(stake_amount, 0)
|
||||||
|
|
||||||
def get_trade_stake_amount(
|
def get_trade_stake_amount(
|
||||||
self, pair: str, max_open_trades: IntOrInf, edge=None, update: bool = True
|
self, pair: str, max_open_trades: IntOrInf, edge=None, update: bool = True
|
||||||
@@ -336,8 +370,8 @@ class Wallets:
|
|||||||
if edge:
|
if edge:
|
||||||
stake_amount = edge.stake_amount(
|
stake_amount = edge.stake_amount(
|
||||||
pair,
|
pair,
|
||||||
self.get_free(self._config["stake_currency"]),
|
self.get_free(self._stake_currency),
|
||||||
self.get_total(self._config["stake_currency"]),
|
self.get_total(self._stake_currency),
|
||||||
val_tied_up,
|
val_tied_up,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
coveralls==4.0.1
|
coveralls==4.0.1
|
||||||
ruff==0.8.1
|
ruff==0.8.2
|
||||||
mypy==1.13.0
|
mypy==1.13.0
|
||||||
pre-commit==4.0.1
|
pre-commit==4.0.1
|
||||||
pytest==8.3.4
|
pytest==8.3.4
|
||||||
@@ -28,5 +28,5 @@ nbconvert==7.16.4
|
|||||||
types-cachetools==5.5.0.20240820
|
types-cachetools==5.5.0.20240820
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.32.0.20241016
|
types-requests==2.32.0.20241016
|
||||||
types-tabulate==0.9.0.20240106
|
types-tabulate==0.9.0.20241207
|
||||||
types-python-dateutil==2.9.0.20241003
|
types-python-dateutil==2.9.0.20241206
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ catboost==1.2.7; 'arm' not in platform_machine
|
|||||||
# Temporary downgrade of matplotlib due to https://github.com/matplotlib/matplotlib/issues/28551
|
# Temporary downgrade of matplotlib due to https://github.com/matplotlib/matplotlib/issues/28551
|
||||||
matplotlib==3.9.3
|
matplotlib==3.9.3
|
||||||
lightgbm==4.5.0
|
lightgbm==4.5.0
|
||||||
xgboost==2.0.3
|
xgboost==2.1.3
|
||||||
tensorboard==2.18.0
|
tensorboard==2.18.0
|
||||||
datasieve==0.1.7
|
datasieve==0.1.7
|
||||||
|
|||||||
+4
-4
@@ -4,12 +4,12 @@ bottleneck==1.4.2
|
|||||||
numexpr==2.10.2
|
numexpr==2.10.2
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==4.4.35
|
ccxt==4.4.37
|
||||||
cryptography==42.0.8; platform_machine == 'armv7l'
|
cryptography==42.0.8; platform_machine == 'armv7l'
|
||||||
cryptography==44.0.0; platform_machine != 'armv7l'
|
cryptography==44.0.0; platform_machine != 'armv7l'
|
||||||
aiohttp==3.10.11
|
aiohttp==3.10.11
|
||||||
SQLAlchemy==2.0.36
|
SQLAlchemy==2.0.36
|
||||||
python-telegram-bot==21.8
|
python-telegram-bot==21.9
|
||||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||||
httpx>=0.24.1
|
httpx>=0.24.1
|
||||||
humanize==4.11.0
|
humanize==4.11.0
|
||||||
@@ -39,8 +39,8 @@ orjson==3.10.12
|
|||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.115.5
|
fastapi==0.115.6
|
||||||
pydantic==2.10.2
|
pydantic==2.10.3
|
||||||
uvicorn==0.32.1
|
uvicorn==0.32.1
|
||||||
pyjwt==2.10.1
|
pyjwt==2.10.1
|
||||||
aiofiles==24.1.0
|
aiofiles==24.1.0
|
||||||
|
|||||||
+27
-2
@@ -1,6 +1,7 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
@@ -517,6 +518,30 @@ def patch_gc(mocker) -> None:
|
|||||||
mocker.patch("freqtrade.main.gc_set_threshold")
|
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)
|
@pytest.fixture(autouse=True)
|
||||||
def user_dir(mocker, tmp_path) -> Path:
|
def user_dir(mocker, tmp_path) -> Path:
|
||||||
user_dir = tmp_path / "user_data"
|
user_dir = tmp_path / "user_data"
|
||||||
@@ -2212,7 +2237,7 @@ def tickers():
|
|||||||
"first": None,
|
"first": None,
|
||||||
"last": 8603.67,
|
"last": 8603.67,
|
||||||
"change": -0.879,
|
"change": -0.879,
|
||||||
"percentage": None,
|
"percentage": -8.95,
|
||||||
"average": None,
|
"average": None,
|
||||||
"baseVolume": 30414.604298,
|
"baseVolume": 30414.604298,
|
||||||
"quoteVolume": 259629896.48584127,
|
"quoteVolume": 259629896.48584127,
|
||||||
@@ -2256,7 +2281,7 @@ def tickers():
|
|||||||
"first": None,
|
"first": None,
|
||||||
"last": 129.28,
|
"last": 129.28,
|
||||||
"change": 1.795,
|
"change": 1.795,
|
||||||
"percentage": None,
|
"percentage": -2.5,
|
||||||
"average": None,
|
"average": None,
|
||||||
"baseVolume": 59698.79897,
|
"baseVolume": 59698.79897,
|
||||||
"quoteVolume": 29132399.743954,
|
"quoteVolume": 29132399.743954,
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ def test_liquidation_price_binance(
|
|||||||
default_conf["trading_mode"] = trading_mode
|
default_conf["trading_mode"] = trading_mode
|
||||||
default_conf["margin_mode"] = margin_mode
|
default_conf["margin_mode"] = margin_mode
|
||||||
default_conf["liquidation_buffer"] = 0.0
|
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")
|
exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
|
||||||
|
|
||||||
def get_maint_ratio(pair_, stake_amount):
|
def get_maint_ratio(pair_, stake_amount):
|
||||||
|
|||||||
@@ -2006,6 +2006,46 @@ def test_get_tickers(default_conf, mocker, exchange_name, caplog):
|
|||||||
assert exchange.get_tickers() == {}
|
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)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_fetch_ticker(default_conf, mocker, exchange_name):
|
def test_fetch_ticker(default_conf, mocker, exchange_name):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
@@ -4079,10 +4119,16 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
|
|||||||
)
|
)
|
||||||
ex = Exchange(default_conf)
|
ex = Exchange(default_conf)
|
||||||
|
|
||||||
assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC"
|
assert next(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("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."):
|
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(
|
@pytest.mark.parametrize(
|
||||||
@@ -6131,6 +6177,7 @@ def test_get_liquidation_price(
|
|||||||
default_conf_usdt["exchange"]["name"] = exchange_name
|
default_conf_usdt["exchange"]["name"] = exchange_name
|
||||||
default_conf_usdt["margin_mode"] = margin_mode
|
default_conf_usdt["margin_mode"] = margin_mode
|
||||||
mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes")
|
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_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name)
|
||||||
|
|
||||||
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
|
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import platform
|
|
||||||
import sys
|
import sys
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -20,30 +19,6 @@ def is_py12() -> bool:
|
|||||||
return sys.version_info >= (3, 12)
|
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")
|
@pytest.fixture(scope="function")
|
||||||
def freqai_conf(default_conf, tmp_path):
|
def freqai_conf(default_conf, tmp_path):
|
||||||
freqaiconf = deepcopy(default_conf)
|
freqaiconf = deepcopy(default_conf)
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ from freqtrade.configuration import TimeRange
|
|||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
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 (
|
from tests.freqai.conftest import (
|
||||||
get_patched_data_kitchen,
|
get_patched_data_kitchen,
|
||||||
get_patched_freqai_strategy,
|
get_patched_freqai_strategy,
|
||||||
is_mac,
|
|
||||||
make_unfiltered_dataframe,
|
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.optimize.backtesting import Backtesting
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||||
from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re
|
from tests.conftest import (
|
||||||
from tests.freqai.conftest import (
|
EXMS,
|
||||||
get_patched_freqai_strategy,
|
create_mock_trades,
|
||||||
|
get_patched_exchange,
|
||||||
is_arm,
|
is_arm,
|
||||||
is_mac,
|
is_mac,
|
||||||
|
log_has_re,
|
||||||
|
)
|
||||||
|
from tests.freqai.conftest import (
|
||||||
|
get_patched_freqai_strategy,
|
||||||
make_rl_config,
|
make_rl_config,
|
||||||
mock_pytorch_mlp_model_training_parameters,
|
mock_pytorch_mlp_model_training_parameters,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4021,7 +4021,7 @@ def test_get_real_amount_fees_order(
|
|||||||
default_conf_usdt, market_buy_order_usdt_doublefee, fee, mocker
|
default_conf_usdt, market_buy_order_usdt_doublefee, fee, mocker
|
||||||
):
|
):
|
||||||
tfo_mock = mocker.patch(f"{EXMS}.get_trades_for_order", return_value=[])
|
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})
|
mocker.patch(f"{EXMS}.fetch_ticker", return_value={"last": 200})
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair="LTC/USDT",
|
pair="LTC/USDT",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def test_update_liquidation_prices(mocker, margin_mode, dry_run):
|
|||||||
|
|
||||||
assert trade_mock.set_liquidation_price.call_count == 1
|
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
|
0 if margin_mode == MarginMode.ISOLATED or not dry_run else 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -568,6 +568,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
|
|||||||
mocker.patch(f"{EXMS}.get_fee", fee)
|
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_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_max_pair_stake_amount", return_value=float("inf"))
|
||||||
|
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
|
||||||
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
|
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
|
||||||
mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p)
|
mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@@ -1842,6 +1843,7 @@ def test_backtest_multi_pair_long_short_switch(
|
|||||||
if use_detail:
|
if use_detail:
|
||||||
default_conf_usdt["timeframe_detail"] = "1m"
|
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_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_max_pair_stake_amount", return_value=float("inf"))
|
||||||
mocker.patch(f"{EXMS}.get_fee", fee)
|
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"})
|
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||||
correct = hl.hyperopt_loss_function(
|
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(
|
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(
|
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 over > correct
|
||||||
assert under > 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"})
|
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||||
longer = hl.hyperopt_loss_function(
|
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
|
assert shorter < longer
|
||||||
|
|
||||||
|
|
||||||
@@ -73,11 +110,34 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
|
|||||||
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
|
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||||
correct = hl.hyperopt_loss_function(
|
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(
|
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 over < correct
|
||||||
assert under > 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})
|
default_conf.update({"hyperopt_loss": lossfunction})
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||||
correct = hl.hyperopt_loss_function(
|
correct = hl.hyperopt_loss_function(
|
||||||
hyperopt_results,
|
results=hyperopt_results,
|
||||||
trade_count=len(hyperopt_results),
|
trade_count=len(hyperopt_results),
|
||||||
min_date=datetime(2019, 1, 1),
|
min_date=datetime(2019, 1, 1),
|
||||||
max_date=datetime(2019, 5, 1),
|
max_date=datetime(2019, 5, 1),
|
||||||
config=default_conf,
|
config=default_conf,
|
||||||
processed=None,
|
processed=None,
|
||||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||||
|
starting_balance=default_conf["dry_run_wallet"],
|
||||||
)
|
)
|
||||||
over = hl.hyperopt_loss_function(
|
over = hl.hyperopt_loss_function(
|
||||||
results_over,
|
results=results_over,
|
||||||
trade_count=len(results_over),
|
trade_count=len(results_over),
|
||||||
min_date=datetime(2019, 1, 1),
|
min_date=datetime(2019, 1, 1),
|
||||||
max_date=datetime(2019, 5, 1),
|
max_date=datetime(2019, 5, 1),
|
||||||
config=default_conf,
|
config=default_conf,
|
||||||
processed=None,
|
processed=None,
|
||||||
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
|
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
|
||||||
|
starting_balance=default_conf["dry_run_wallet"],
|
||||||
)
|
)
|
||||||
under = hl.hyperopt_loss_function(
|
under = hl.hyperopt_loss_function(
|
||||||
results_under,
|
results=results_under,
|
||||||
trade_count=len(results_under),
|
trade_count=len(results_under),
|
||||||
min_date=datetime(2019, 1, 1),
|
min_date=datetime(2019, 1, 1),
|
||||||
max_date=datetime(2019, 5, 1),
|
max_date=datetime(2019, 5, 1),
|
||||||
config=default_conf,
|
config=default_conf,
|
||||||
processed=None,
|
processed=None,
|
||||||
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
|
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
|
||||||
|
starting_balance=default_conf["dry_run_wallet"],
|
||||||
)
|
)
|
||||||
assert over < correct
|
assert over < correct
|
||||||
assert under > correct
|
assert under > correct
|
||||||
|
|||||||
@@ -360,9 +360,16 @@ def test_gen_pairlist_from_tickers(mocker, rpl_config, tickers):
|
|||||||
exchange = get_patched_exchange(mocker, rpl_config, exchange="binance")
|
exchange = get_patched_exchange(mocker, rpl_config, exchange="binance")
|
||||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||||
|
|
||||||
remote_pairlist = PercentChangePairList(
|
remote_pairlist = pairlistmanager._pairlist_handlers[0]
|
||||||
exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 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)
|
result = remote_pairlist.gen_pairlist(tickers.return_value)
|
||||||
|
|
||||||
|
|||||||
+32
-4
@@ -514,8 +514,13 @@ def test_rpc_balance_handle_error(default_conf, mocker):
|
|||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
rpc._fiat_converter = CryptoToFiatConverter({})
|
rpc._fiat_converter = CryptoToFiatConverter({})
|
||||||
with pytest.raises(RPCException, match="Error getting current tickers."):
|
res = rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"])
|
||||||
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):
|
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,
|
"total": 5.0,
|
||||||
"used": 4.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": {
|
"USDT": {
|
||||||
"free": 50.0,
|
"free": 50.0,
|
||||||
"total": 100.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),
|
fetch_positions=MagicMock(return_value=mock_pos),
|
||||||
get_tickers=tickers,
|
get_tickers=tickers,
|
||||||
get_valid_pair_combination=MagicMock(
|
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
|
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["total"]) == 2824.83464
|
||||||
assert pytest.approx(result["value"]) == 2824.83464 * 1.2
|
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
|
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 "USD" == result["symbol"]
|
||||||
assert result["currencies"] == [
|
assert result["currencies"] == [
|
||||||
{
|
{
|
||||||
@@ -622,6 +636,20 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
|
|||||||
"is_bot_managed": False,
|
"is_bot_managed": False,
|
||||||
"is_position": 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",
|
"currency": "USDT",
|
||||||
"free": 50.0,
|
"free": 50.0,
|
||||||
|
|||||||
@@ -556,7 +556,7 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers):
|
|||||||
ftbot.config["dry_run"] = False
|
ftbot.config["dry_run"] = False
|
||||||
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
|
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
|
||||||
mocker.patch(f"{EXMS}.get_tickers", tickers)
|
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()
|
ftbot.wallets.update()
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/balance")
|
rc = client_get(client, f"{BASE_URI}/balance")
|
||||||
|
|||||||
@@ -960,7 +960,7 @@ async def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance
|
|||||||
default_conf["dry_run"] = False
|
default_conf["dry_run"] = False
|
||||||
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
|
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
|
||||||
mocker.patch(f"{EXMS}.get_tickers", tickers)
|
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)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
patch_get_signal(freqtradebot)
|
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}.get_balances", return_value=rpc_balance)
|
||||||
mocker.patch(f"{EXMS}.fetch_positions", return_value=mock_pos)
|
mocker.patch(f"{EXMS}.fetch_positions", return_value=mock_pos)
|
||||||
mocker.patch(f"{EXMS}.get_tickers", tickers)
|
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)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
|
|||||||
@@ -1481,6 +1481,12 @@ def test_flat_vars_to_nested_dict(caplog):
|
|||||||
"FREQTRADE__STAKE_AMOUNT": "200.05",
|
"FREQTRADE__STAKE_AMOUNT": "200.05",
|
||||||
"FREQTRADE__TELEGRAM__CHAT_ID": "2151",
|
"FREQTRADE__TELEGRAM__CHAT_ID": "2151",
|
||||||
"NOT_RELEVANT": "200.0", # Will be ignored
|
"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 = {
|
expected = {
|
||||||
"stake_amount": 200.05,
|
"stake_amount": 200.05,
|
||||||
@@ -1494,8 +1500,12 @@ def test_flat_vars_to_nested_dict(caplog):
|
|||||||
},
|
},
|
||||||
"some_setting": True,
|
"some_setting": True,
|
||||||
"some_false_setting": False,
|
"some_false_setting": False,
|
||||||
|
"pair_whitelist": ["BTC/USDT", "ETH/USDT"],
|
||||||
},
|
},
|
||||||
"telegram": {"chat_id": "2151"},
|
"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)
|
res = _flat_vars_to_nested_dict(test_args, ENV_VAR_PREFIX)
|
||||||
assert res == expected
|
assert res == expected
|
||||||
|
|||||||
+149
-1
@@ -168,7 +168,7 @@ def test_get_trade_stake_amount_unlimited_amount(
|
|||||||
assert result == 0
|
assert result == 0
|
||||||
|
|
||||||
freqtrade.config["dry_run_wallet"] = 200
|
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)
|
result = freqtrade.wallets.get_trade_stake_amount("XRP/USDT", 3)
|
||||||
assert round(result, 4) == round(result2, 4)
|
assert round(result, 4) == round(result2, 4)
|
||||||
|
|
||||||
@@ -451,3 +451,151 @@ def test_check_exit_amount_futures(mocker, default_conf, fee):
|
|||||||
assert freqtrade.wallets.check_exit_amount(trade) is False
|
assert freqtrade.wallets.check_exit_amount(trade) is False
|
||||||
assert total_mock.call_count == 0
|
assert total_mock.call_count == 0
|
||||||
assert update_mock.call_count == 1
|
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
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user