mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-12-02 10:03:05 +00:00
Merge pull request #11000 from freqtrade/feat/multi_wallet
Dry-run multi-wallet support
This commit is contained in:
@@ -102,8 +102,17 @@
|
||||
},
|
||||
"dry_run_wallet": {
|
||||
"description": "Initial wallet balance for dry run mode.",
|
||||
"type": "number",
|
||||
"default": 1000
|
||||
"type": [
|
||||
"number",
|
||||
"object"
|
||||
],
|
||||
"default": 1000,
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9]+$": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"cancel_open_orders_on_exit": {
|
||||
"description": "Cancel open orders when exiting.",
|
||||
|
||||
@@ -39,6 +39,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
|
||||
config: Config,
|
||||
processed: dict[str, DataFrame],
|
||||
backtest_stats: dict[str, Any],
|
||||
starting_balance: float,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
"""
|
||||
@@ -70,6 +71,7 @@ Currently, the arguments are:
|
||||
* `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space).
|
||||
* `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting.
|
||||
* `backtest_stats`: Backtesting statistics using the same format as the backtesting file "strategy" substructure. Available fields can be seen in `generate_strategy_stats()` in `optimize_reports.py`.
|
||||
* `starting_balance`: Starting balance used for backtesting.
|
||||
|
||||
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `timeframe` | The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). Usually missing in configuration, and specified in the strategy. [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
||||
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
||||
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
|
||||
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode. [More information below](#dry-run-wallet)<br>*Defaults to `1000`.* <br> **Datatype:** Float or Dict
|
||||
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to exit a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||
@@ -324,6 +324,25 @@ To limit this calculation in case of large stoploss values, the calculated minim
|
||||
!!! Warning
|
||||
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. Freqtrade adjusts the stake-amount to this value, unless it's > 30% more than the calculated/desired stake-amount - in which case the trade is rejected.
|
||||
|
||||
#### Dry-run wallet
|
||||
|
||||
When running in dry-run mode, the bot will use a simulated wallet to execute trades. The starting balance of this wallet is defined by `dry_run_wallet` (defaults to 1000).
|
||||
For more complex scenarios, you can also assign a dictionary to `dry_run_wallet` to define the starting balance for each currency.
|
||||
|
||||
```json
|
||||
"dry_run_wallet": {
|
||||
"BTC": 0.01,
|
||||
"ETH": 2,
|
||||
"USDT": 1000
|
||||
}
|
||||
```
|
||||
|
||||
Command line options (`--dry-run-wallet`) can be used to override the configuration value, but only for the float value, not for the dictionary. If you'd like to use the dictionary, please adjust the configuration file.
|
||||
|
||||
!!! Note
|
||||
Balances not in stake-currency will not be used for trading, but are shown as part of the wallet balance.
|
||||
On Cross-margin exchanges, the wallet balance may be used to calculate the available collateral for trading.
|
||||
|
||||
#### Tradable balance
|
||||
|
||||
By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade.
|
||||
|
||||
@@ -17,7 +17,7 @@ def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[
|
||||
:return: Configuration
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.util import fmt_coin
|
||||
from freqtrade.util import fmt_coin, get_dry_run_wallet
|
||||
|
||||
config = setup_utils_configuration(args, method)
|
||||
|
||||
@@ -26,7 +26,7 @@ def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[
|
||||
RunMode.HYPEROPT: "hyperoptimization",
|
||||
}
|
||||
if method in no_unlimited_runmodes.keys():
|
||||
wallet_size = config["dry_run_wallet"] * config["tradable_balance_ratio"]
|
||||
wallet_size = get_dry_run_wallet(config) * config["tradable_balance_ratio"]
|
||||
# tradable_balance_ratio
|
||||
if (
|
||||
config["stake_amount"] != constants.UNLIMITED_STAKE_AMOUNT
|
||||
|
||||
@@ -85,8 +85,10 @@ CONF_SCHEMA = {
|
||||
},
|
||||
"dry_run_wallet": {
|
||||
"description": "Initial wallet balance for dry run mode.",
|
||||
"type": "number",
|
||||
"type": ["number", "object"],
|
||||
"default": DRY_RUN_WALLET,
|
||||
"patternProperties": {r"^[a-zA-Z0-9]+$": {"type": "number"}},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
"cancel_open_orders_on_exit": {
|
||||
"description": "Cancel open orders when exiting.",
|
||||
|
||||
@@ -57,8 +57,14 @@ class Binance(Exchange):
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||
]
|
||||
|
||||
def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers:
|
||||
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
||||
def get_tickers(
|
||||
self,
|
||||
symbols: list[str] | None = None,
|
||||
*,
|
||||
cached: bool = False,
|
||||
market_type: TradingMode | None = None,
|
||||
) -> Tickers:
|
||||
tickers = super().get_tickers(symbols=symbols, cached=cached, market_type=market_type)
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
# Binance's future result has no bid/ask values.
|
||||
# Therefore we must fetch that from fetch_bids_asks and combine the two results.
|
||||
|
||||
@@ -201,7 +201,7 @@ class Exchange:
|
||||
|
||||
self._cache_lock = Lock()
|
||||
# Cache for 10 minutes ...
|
||||
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
|
||||
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=4, ttl=60 * 10)
|
||||
# Cache values for 300 to avoid frequent polling of the exchange for prices
|
||||
# Caching only applies to RPC methods, so prices for open trades are still
|
||||
# refreshed once every iteration.
|
||||
@@ -1801,24 +1801,37 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers:
|
||||
def get_tickers(
|
||||
self,
|
||||
symbols: list[str] | None = None,
|
||||
*,
|
||||
cached: bool = False,
|
||||
market_type: TradingMode | None = None,
|
||||
) -> Tickers:
|
||||
"""
|
||||
:param symbols: List of symbols to fetch
|
||||
:param cached: Allow cached result
|
||||
:param market_type: Market type to fetch - either spot or futures.
|
||||
:return: fetch_tickers result
|
||||
"""
|
||||
tickers: Tickers
|
||||
if not self.exchange_has("fetchTickers"):
|
||||
return {}
|
||||
cache_key = f"fetch_tickers_{market_type}" if market_type else "fetch_tickers"
|
||||
if cached:
|
||||
with self._cache_lock:
|
||||
tickers = self._fetch_tickers_cache.get("fetch_tickers") # type: ignore
|
||||
tickers = self._fetch_tickers_cache.get(cache_key) # type: ignore
|
||||
if tickers:
|
||||
return tickers
|
||||
try:
|
||||
tickers = self._api.fetch_tickers(symbols)
|
||||
# Re-map futures to swap
|
||||
market_types = {
|
||||
TradingMode.FUTURES: "swap",
|
||||
}
|
||||
params = {"type": market_types.get(market_type, market_type)} if market_type else {}
|
||||
tickers = self._api.fetch_tickers(symbols, params)
|
||||
with self._cache_lock:
|
||||
self._fetch_tickers_cache["fetch_tickers"] = tickers
|
||||
self._fetch_tickers_cache[cache_key] = tickers
|
||||
return tickers
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
|
||||
@@ -50,11 +50,17 @@ class Kraken(Exchange):
|
||||
|
||||
return parent_check and market.get("darkpool", False) is False
|
||||
|
||||
def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers:
|
||||
def get_tickers(
|
||||
self,
|
||||
symbols: list[str] | None = None,
|
||||
*,
|
||||
cached: bool = False,
|
||||
market_type: TradingMode | None = None,
|
||||
) -> Tickers:
|
||||
# Only fetch tickers for current stake currency
|
||||
# Otherwise the request for kraken becomes too large.
|
||||
symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]]))
|
||||
return super().get_tickers(symbols=symbols, cached=cached)
|
||||
return super().get_tickers(symbols=symbols, cached=cached, market_type=market_type)
|
||||
|
||||
def consolidate_balances(self, balances: CcxtBalances) -> CcxtBalances:
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,7 @@ from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.optimize.analysis.lookahead import LookaheadAnalysis
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.util import print_rich_table
|
||||
from freqtrade.util import get_dry_run_wallet, print_rich_table
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -163,7 +163,7 @@ class LookaheadAnalysisSubFunctions:
|
||||
config["max_open_trades"] = len(config["pairs"])
|
||||
|
||||
min_dry_run_wallet = 1000000000
|
||||
if config["dry_run_wallet"] < min_dry_run_wallet:
|
||||
if get_dry_run_wallet(config) < min_dry_run_wallet:
|
||||
logger.info(
|
||||
"Dry run wallet was not set to 1 billion, pushing it up there "
|
||||
"just to avoid false positives"
|
||||
|
||||
@@ -28,6 +28,7 @@ from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLo
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer, HyperoptTools
|
||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
|
||||
|
||||
|
||||
# Suppress scikit-learn FutureWarnings from skopt
|
||||
@@ -363,6 +364,7 @@ class HyperOptimizer:
|
||||
config=self.config,
|
||||
processed=processed,
|
||||
backtest_stats=strat_stats,
|
||||
starting_balance=get_dry_run_wallet(self.config),
|
||||
)
|
||||
return {
|
||||
"loss": loss,
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_calmar
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -24,10 +23,9 @@ class CalmarHyperOptLoss(IHyperOptLoss):
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Config,
|
||||
starting_balance: float,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
@@ -36,7 +34,6 @@ class CalmarHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
Uses Calmar Ratio calculation.
|
||||
"""
|
||||
starting_balance = config["dry_run_wallet"]
|
||||
calmar_ratio = calculate_calmar(results, min_date, max_date, starting_balance)
|
||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||
return -calmar_ratio
|
||||
|
||||
@@ -31,6 +31,7 @@ class IHyperOptLoss(ABC):
|
||||
config: Config,
|
||||
processed: dict[str, DataFrame],
|
||||
backtest_stats: dict[str, Any],
|
||||
starting_balance: float,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
"""
|
||||
|
||||
@@ -7,7 +7,6 @@ Hyperoptimization.
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_underwater
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -21,7 +20,9 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, config: Config, *args, **kwargs) -> float:
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame, starting_balance: float, *args, **kwargs
|
||||
) -> float:
|
||||
"""
|
||||
Objective function.
|
||||
|
||||
@@ -31,7 +32,7 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss):
|
||||
total_profit = results["profit_abs"].sum()
|
||||
try:
|
||||
drawdown_df = calculate_underwater(
|
||||
results, value_col="profit_abs", starting_balance=config["dry_run_wallet"]
|
||||
results, value_col="profit_abs", starting_balance=starting_balance
|
||||
)
|
||||
max_drawdown = abs(min(drawdown_df["drawdown"]))
|
||||
relative_drawdown = max(drawdown_df["drawdown_relative"])
|
||||
|
||||
@@ -33,7 +33,6 @@ TARGET_TRADE_AMOUNT variable sets the minimum number of trades required to avoid
|
||||
import numpy as np
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -57,7 +56,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss):
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
config: Config,
|
||||
starting_balance: float,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
total_profit = results["profit_abs"].sum()
|
||||
@@ -83,7 +82,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss):
|
||||
# Calculate drawdown
|
||||
try:
|
||||
drawdown = calculate_max_drawdown(
|
||||
results, starting_balance=config["dry_run_wallet"], value_col="profit_abs"
|
||||
results, starting_balance=starting_balance, value_col="profit_abs"
|
||||
)
|
||||
relative_account_drawdown = drawdown.relative_account_drawdown
|
||||
except ValueError:
|
||||
|
||||
@@ -10,7 +10,6 @@ individual needs.
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_max_drawdown
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -21,12 +20,14 @@ DRAWDOWN_MULT = 0.075
|
||||
|
||||
class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, config: Config, *args, **kwargs) -> float:
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame, starting_balance: float, *args, **kwargs
|
||||
) -> float:
|
||||
total_profit = results["profit_abs"].sum()
|
||||
|
||||
try:
|
||||
drawdown = calculate_max_drawdown(
|
||||
results, starting_balance=config["dry_run_wallet"], value_col="profit_abs"
|
||||
results, starting_balance=starting_balance, value_col="profit_abs"
|
||||
)
|
||||
relative_account_drawdown = drawdown.relative_account_drawdown
|
||||
except ValueError:
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_sharpe
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -24,10 +23,9 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Config,
|
||||
starting_balance: float,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
@@ -36,7 +34,6 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
Uses Sharpe Ratio calculation.
|
||||
"""
|
||||
starting_balance = config["dry_run_wallet"]
|
||||
sharp_ratio = calculate_sharpe(results, min_date, max_date, starting_balance)
|
||||
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||
return -sharp_ratio
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.metrics import calculate_sortino
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
@@ -24,10 +23,9 @@ class SortinoHyperOptLoss(IHyperOptLoss):
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Config,
|
||||
starting_balance: float,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
@@ -36,7 +34,6 @@ class SortinoHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
Uses Sortino Ratio calculation.
|
||||
"""
|
||||
starting_balance = config["dry_run_wallet"]
|
||||
sortino_ratio = calculate_sortino(results, min_date, max_date, starting_balance)
|
||||
# print(expected_returns_mean, down_stdev, sortino_ratio)
|
||||
return -sortino_ratio
|
||||
|
||||
@@ -18,7 +18,7 @@ from freqtrade.data.metrics import (
|
||||
calculate_sortino,
|
||||
)
|
||||
from freqtrade.ft_types import BacktestResultType
|
||||
from freqtrade.util import decimals_per_coin, fmt_coin
|
||||
from freqtrade.util import decimals_per_coin, fmt_coin, get_dry_run_wallet
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -69,7 +69,7 @@ def generate_rejected_signals(
|
||||
|
||||
|
||||
def _generate_result_line(
|
||||
result: DataFrame, starting_balance: int, first_column: str | list[str]
|
||||
result: DataFrame, starting_balance: float, first_column: str | list[str]
|
||||
) -> dict:
|
||||
"""
|
||||
Generate one result dict, with "first_column" as key.
|
||||
@@ -111,7 +111,7 @@ def _generate_result_line(
|
||||
def generate_pair_metrics(
|
||||
pairlist: list[str],
|
||||
stake_currency: str,
|
||||
starting_balance: int,
|
||||
starting_balance: float,
|
||||
results: DataFrame,
|
||||
skip_nan: bool = False,
|
||||
) -> list[dict]:
|
||||
@@ -144,7 +144,7 @@ def generate_pair_metrics(
|
||||
|
||||
def generate_tag_metrics(
|
||||
tag_type: Literal["enter_tag", "exit_reason"] | list[Literal["enter_tag", "exit_reason"]],
|
||||
starting_balance: int,
|
||||
starting_balance: float,
|
||||
results: DataFrame,
|
||||
skip_nan: bool = False,
|
||||
) -> list[dict]:
|
||||
@@ -373,7 +373,7 @@ def generate_strategy_stats(
|
||||
return {}
|
||||
config = content["config"]
|
||||
max_open_trades = min(config["max_open_trades"], len(pairlist))
|
||||
start_balance = config["dry_run_wallet"]
|
||||
start_balance = get_dry_run_wallet(config)
|
||||
stake_currency = config["stake_currency"]
|
||||
|
||||
pair_results = generate_pair_metrics(
|
||||
|
||||
@@ -28,6 +28,7 @@ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy import IStrategy
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.util import get_dry_run_wallet
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -706,7 +707,7 @@ def plot_profit(config: Config) -> None:
|
||||
trades,
|
||||
config["timeframe"],
|
||||
config.get("stake_currency", ""),
|
||||
config.get("available_capital", config["dry_run_wallet"]),
|
||||
config.get("available_capital", get_dry_run_wallet(config)),
|
||||
)
|
||||
store_plot_file(
|
||||
fig,
|
||||
|
||||
@@ -7,7 +7,7 @@ from abc import abstractmethod
|
||||
from collections.abc import Generator, Sequence
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from math import isnan
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
import psutil
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@@ -682,12 +682,23 @@ class RPC:
|
||||
est_bot_stake = amount
|
||||
else:
|
||||
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
||||
rate: float | None = cast(Ticker, tickers.get(pair, {})).get("last", None)
|
||||
if rate:
|
||||
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
||||
rate = 1.0 / rate
|
||||
est_stake = rate * balance.total
|
||||
est_bot_stake = rate * amount
|
||||
ticker: Ticker | None = tickers.get(pair, None)
|
||||
if not ticker:
|
||||
tickers_spot: Tickers = self._freqtrade.exchange.get_tickers(
|
||||
cached=True,
|
||||
market_type=TradingMode.SPOT
|
||||
if self._config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT
|
||||
else TradingMode.FUTURES,
|
||||
)
|
||||
ticker = tickers_spot.get(pair, None)
|
||||
|
||||
if ticker:
|
||||
rate: float | None = ticker.get("last", None)
|
||||
if rate:
|
||||
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
||||
rate = 1.0 / rate
|
||||
est_stake = rate * balance.total
|
||||
est_bot_stake = rate * amount
|
||||
|
||||
return est_stake, est_bot_stake
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from freqtrade.util.datetime_helpers import (
|
||||
format_ms_time,
|
||||
shorten_date,
|
||||
)
|
||||
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
|
||||
from freqtrade.util.formatters import decimals_per_coin, fmt_coin, fmt_coin2, round_value
|
||||
from freqtrade.util.ft_precise import FtPrecise
|
||||
from freqtrade.util.measure_time import MeasureTime
|
||||
@@ -35,6 +36,7 @@ __all__ = [
|
||||
"dt_utc",
|
||||
"format_date",
|
||||
"format_ms_time",
|
||||
"get_dry_run_wallet",
|
||||
"FtPrecise",
|
||||
"PeriodicCache",
|
||||
"shorten_date",
|
||||
|
||||
12
freqtrade/util/dry_run_wallet.py
Normal file
12
freqtrade/util/dry_run_wallet.py
Normal file
@@ -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")
|
||||
@@ -41,7 +41,14 @@ class Wallets:
|
||||
self._exchange = exchange
|
||||
self._wallets: dict[str, Wallet] = {}
|
||||
self._positions: dict[str, PositionWallet] = {}
|
||||
self._start_cap = config["dry_run_wallet"]
|
||||
self._start_cap: dict[str, float] = {}
|
||||
self._stake_currency = config["stake_currency"]
|
||||
|
||||
if isinstance(_start_cap := config["dry_run_wallet"], float | int):
|
||||
self._start_cap[self._stake_currency] = _start_cap
|
||||
else:
|
||||
self._start_cap = _start_cap
|
||||
|
||||
self._last_wallet_refresh: datetime | None = None
|
||||
self.update()
|
||||
|
||||
@@ -109,10 +116,18 @@ class Wallets:
|
||||
for o in trade.open_orders
|
||||
if o.amount and o.ft_order_side == trade.exit_side
|
||||
)
|
||||
curr_wallet_bal = self._start_cap.get(curr, 0)
|
||||
|
||||
_wallets[curr] = Wallet(curr, trade.amount - pending, pending, trade.amount)
|
||||
_wallets[curr] = Wallet(
|
||||
curr,
|
||||
curr_wallet_bal + trade.amount - pending,
|
||||
pending,
|
||||
trade.amount + curr_wallet_bal,
|
||||
)
|
||||
|
||||
current_stake = self._start_cap + tot_profit - tot_in_trades
|
||||
current_stake = (
|
||||
self._start_cap.get(self._stake_currency, 0) + tot_profit - tot_in_trades
|
||||
)
|
||||
total_stake = current_stake + used_stake
|
||||
else:
|
||||
tot_in_trades = 0
|
||||
@@ -129,16 +144,24 @@ class Wallets:
|
||||
collateral=collateral,
|
||||
side=position.trade_direction,
|
||||
)
|
||||
current_stake = self._start_cap + tot_profit - tot_in_trades
|
||||
current_stake = (
|
||||
self._start_cap.get(self._stake_currency, 0) + tot_profit - tot_in_trades
|
||||
)
|
||||
|
||||
used_stake = tot_in_trades
|
||||
total_stake = current_stake + tot_in_trades
|
||||
|
||||
_wallets[self._config["stake_currency"]] = Wallet(
|
||||
currency=self._config["stake_currency"],
|
||||
_wallets[self._stake_currency] = Wallet(
|
||||
currency=self._stake_currency,
|
||||
free=current_stake,
|
||||
used=used_stake,
|
||||
total=total_stake,
|
||||
)
|
||||
for currency in self._start_cap:
|
||||
if currency not in _wallets:
|
||||
bal = self._start_cap[currency]
|
||||
_wallets[currency] = Wallet(currency, bal, 0, bal)
|
||||
|
||||
self._wallets = _wallets
|
||||
self._positions = _positions
|
||||
|
||||
@@ -244,7 +267,7 @@ class Wallets:
|
||||
else:
|
||||
tot_profit = Trade.get_total_closed_profit()
|
||||
open_stakes = Trade.total_open_trades_stakes()
|
||||
available_balance = self.get_free(self._config["stake_currency"])
|
||||
available_balance = self.get_free(self._stake_currency)
|
||||
return available_balance - tot_profit + open_stakes
|
||||
|
||||
def get_total_stake_amount(self):
|
||||
@@ -264,9 +287,9 @@ class Wallets:
|
||||
# Ensure <tradable_balance_ratio>% is used from the overall balance
|
||||
# Otherwise we'd risk lowering stakes with each open trade.
|
||||
# (tied up + current free) * ratio) - tied up
|
||||
available_amount = (
|
||||
val_tied_up + self.get_free(self._config["stake_currency"])
|
||||
) * self._config["tradable_balance_ratio"]
|
||||
available_amount = (val_tied_up + self.get_free(self._stake_currency)) * self._config[
|
||||
"tradable_balance_ratio"
|
||||
]
|
||||
return available_amount
|
||||
|
||||
def get_available_stake_amount(self) -> float:
|
||||
@@ -277,7 +300,7 @@ class Wallets:
|
||||
(<open_trade stakes> + free amount) * tradable_balance_ratio - <open_trade stakes>
|
||||
"""
|
||||
|
||||
free = self.get_free(self._config["stake_currency"])
|
||||
free = self.get_free(self._stake_currency)
|
||||
return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free)
|
||||
|
||||
def _calculate_unlimited_stake_amount(
|
||||
@@ -336,8 +359,8 @@ class Wallets:
|
||||
if edge:
|
||||
stake_amount = edge.stake_amount(
|
||||
pair,
|
||||
self.get_free(self._config["stake_currency"]),
|
||||
self.get_total(self._config["stake_currency"]),
|
||||
self.get_free(self._stake_currency),
|
||||
self.get_total(self._stake_currency),
|
||||
val_tied_up,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -39,13 +39,34 @@ def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_res
|
||||
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
|
||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||
correct = hl.hyperopt_loss_function(
|
||||
hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=hyperopt_results,
|
||||
trade_count=600,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
over = hl.hyperopt_loss_function(
|
||||
hyperopt_results, 600 + 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=hyperopt_results,
|
||||
trade_count=600 + 100,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
under = hl.hyperopt_loss_function(
|
||||
hyperopt_results, 600 - 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=hyperopt_results,
|
||||
trade_count=600 - 100,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
assert over > correct
|
||||
assert under > correct
|
||||
@@ -58,9 +79,25 @@ def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results)
|
||||
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
|
||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||
longer = hl.hyperopt_loss_function(
|
||||
hyperopt_results, 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=hyperopt_results,
|
||||
trade_count=100,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
shorter = hl.hyperopt_loss_function(
|
||||
results=resultsb,
|
||||
trade_count=100,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": resultsb["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
shorter = hl.hyperopt_loss_function(resultsb, 100, datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
assert shorter < longer
|
||||
|
||||
|
||||
@@ -73,11 +110,34 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
|
||||
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
|
||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||
correct = hl.hyperopt_loss_function(
|
||||
hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=hyperopt_results,
|
||||
trade_count=600,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
over = hl.hyperopt_loss_function(
|
||||
results=results_over,
|
||||
trade_count=600,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
over = hl.hyperopt_loss_function(results_over, 600, datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
under = hl.hyperopt_loss_function(
|
||||
results_under, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
|
||||
results=results_under,
|
||||
trade_count=600,
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=hyperopt_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
|
||||
starting_balance=hyperopt_conf["dry_run_wallet"],
|
||||
)
|
||||
assert over < correct
|
||||
assert under > correct
|
||||
@@ -109,31 +169,34 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct
|
||||
default_conf.update({"hyperopt_loss": lossfunction})
|
||||
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||
correct = hl.hyperopt_loss_function(
|
||||
hyperopt_results,
|
||||
results=hyperopt_results,
|
||||
trade_count=len(hyperopt_results),
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=default_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
|
||||
starting_balance=default_conf["dry_run_wallet"],
|
||||
)
|
||||
over = hl.hyperopt_loss_function(
|
||||
results_over,
|
||||
results=results_over,
|
||||
trade_count=len(results_over),
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=default_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
|
||||
starting_balance=default_conf["dry_run_wallet"],
|
||||
)
|
||||
under = hl.hyperopt_loss_function(
|
||||
results_under,
|
||||
results=results_under,
|
||||
trade_count=len(results_under),
|
||||
min_date=datetime(2019, 1, 1),
|
||||
max_date=datetime(2019, 5, 1),
|
||||
config=default_conf,
|
||||
processed=None,
|
||||
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
|
||||
starting_balance=default_conf["dry_run_wallet"],
|
||||
)
|
||||
assert over < correct
|
||||
assert under > correct
|
||||
|
||||
@@ -530,6 +530,13 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
|
||||
"total": 5.0,
|
||||
"used": 4.0,
|
||||
},
|
||||
# Invalid coin not in tickers list.
|
||||
# This triggers a 2nd call to get_tickers
|
||||
"NotACoin": {
|
||||
"free": 0.0,
|
||||
"total": 2.0,
|
||||
"used": 0.0,
|
||||
},
|
||||
"USDT": {
|
||||
"free": 50.0,
|
||||
"total": 100.0,
|
||||
@@ -590,8 +597,10 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
|
||||
|
||||
assert pytest.approx(result["total"]) == 2824.83464
|
||||
assert pytest.approx(result["value"]) == 2824.83464 * 1.2
|
||||
assert tickers.call_count == 1
|
||||
assert tickers.call_count == 2
|
||||
assert tickers.call_args_list[0][1]["cached"] is True
|
||||
# Testing futures - so we should get spot tickers
|
||||
assert tickers.call_args_list[1][1]["market_type"] == "spot"
|
||||
assert "USD" == result["symbol"]
|
||||
assert result["currencies"] == [
|
||||
{
|
||||
@@ -622,6 +631,20 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
|
||||
"is_bot_managed": False,
|
||||
"is_position": False,
|
||||
},
|
||||
{
|
||||
"currency": "NotACoin",
|
||||
"balance": 2.0,
|
||||
"bot_owned": 0,
|
||||
"est_stake": 0,
|
||||
"est_stake_bot": 0,
|
||||
"free": 0.0,
|
||||
"is_bot_managed": False,
|
||||
"is_position": False,
|
||||
"position": 0,
|
||||
"side": "long",
|
||||
"stake": "USDT",
|
||||
"used": 0.0,
|
||||
},
|
||||
{
|
||||
"currency": "USDT",
|
||||
"free": 50.0,
|
||||
|
||||
@@ -168,7 +168,7 @@ def test_get_trade_stake_amount_unlimited_amount(
|
||||
assert result == 0
|
||||
|
||||
freqtrade.config["dry_run_wallet"] = 200
|
||||
freqtrade.wallets._start_cap = 200
|
||||
freqtrade.wallets._start_cap["BTC"] = 200
|
||||
result = freqtrade.wallets.get_trade_stake_amount("XRP/USDT", 3)
|
||||
assert round(result, 4) == round(result2, 4)
|
||||
|
||||
@@ -451,3 +451,63 @@ def test_check_exit_amount_futures(mocker, default_conf, fee):
|
||||
assert freqtrade.wallets.check_exit_amount(trade) is False
|
||||
assert total_mock.call_count == 0
|
||||
assert update_mock.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config,wallets",
|
||||
[
|
||||
(
|
||||
{"stake_currency": "USDT", "dry_run_wallet": 1000.0},
|
||||
{"USDT": {"currency": "USDT", "free": 1000.0, "used": 0.0, "total": 1000.0}},
|
||||
),
|
||||
(
|
||||
{"stake_currency": "USDT", "dry_run_wallet": {"USDT": 1000.0, "BTC": 0.1, "ETH": 2.0}},
|
||||
{
|
||||
"USDT": {"currency": "USDT", "free": 1000.0, "used": 0.0, "total": 1000.0},
|
||||
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
|
||||
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallets):
|
||||
default_conf_usdt.update(config)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
# 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,
|
||||
},
|
||||
)
|
||||
freqtrade.execute_entry("NEO/USDT", 100.0)
|
||||
|
||||
# Update wallets and verify NEO is now included
|
||||
freqtrade.wallets.update()
|
||||
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
|
||||
|
||||
# Verify USDT wallet was reduced by trade amount
|
||||
assert (
|
||||
pytest.approx(freqtrade.wallets._wallets["USDT"].total) == wallets["USDT"]["total"] - 100.0
|
||||
)
|
||||
assert len(freqtrade.wallets._wallets) == len(wallets) + 1 # Original wallets + NEO
|
||||
|
||||
Reference in New Issue
Block a user