Skip to content

Commit

Permalink
Merge pull request freqtrade#11000 from freqtrade/feat/multi_wallet
Browse files Browse the repository at this point in the history
Dry-run multi-wallet support
  • Loading branch information
xmatthias authored Dec 7, 2024
2 parents fb9e11b + 98e0a5f commit a85f200
Show file tree
Hide file tree
Showing 26 changed files with 324 additions and 77 deletions.
13 changes: 11 additions & 2 deletions build_helpers/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions docs/advanced-hyperopt.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
config: Config,
processed: dict[str, DataFrame],
backtest_stats: dict[str, Any],
starting_balance: float,
**kwargs,
) -> float:
"""
Expand Down Expand Up @@ -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.

Expand Down
21 changes: 20 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions freqtrade/commands/optimize_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion freqtrade/configuration/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@
},
"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.",
Expand Down
10 changes: 8 additions & 2 deletions freqtrade/exchange/binance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 18 additions & 5 deletions freqtrade/exchange/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def __init__(

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.
Expand Down Expand Up @@ -1801,24 +1801,37 @@ def fetch_bids_asks(self, symbols: list[str] | None = None, *, cached: bool = Fa
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(
Expand Down
10 changes: 8 additions & 2 deletions freqtrade/exchange/kraken.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,17 @@ def market_is_tradable(self, market: dict[str, Any]) -> bool:

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:
"""
Expand Down
4 changes: 2 additions & 2 deletions freqtrade/optimize/analysis/lookahead_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
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__)
Expand Down Expand Up @@ -163,7 +163,7 @@ def calculate_config_overrides(config: Config):
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"
Expand Down
2 changes: 2 additions & 0 deletions freqtrade/optimize/hyperopt/hyperopt_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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
Expand Down Expand Up @@ -363,6 +364,7 @@ def _get_results_dict(
config=self.config,
processed=processed,
backtest_stats=strat_stats,
starting_balance=get_dry_run_wallet(self.config),
)
return {
"loss": loss,
Expand Down
5 changes: 1 addition & 4 deletions freqtrade/optimize/hyperopt_loss/hyperopt_loss_calmar.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

from pandas import DataFrame

from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_calmar
from freqtrade.optimize.hyperopt import IHyperOptLoss

Expand All @@ -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:
Expand All @@ -36,7 +34,6 @@ def hyperopt_loss_function(
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def hyperopt_loss_function(
config: Config,
processed: dict[str, DataFrame],
backtest_stats: dict[str, Any],
starting_balance: float,
**kwargs,
) -> float:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from pandas import DataFrame

from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_underwater
from freqtrade.optimize.hyperopt import IHyperOptLoss

Expand All @@ -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.
Expand All @@ -31,7 +32,7 @@ def hyperopt_loss_function(results: DataFrame, config: Config, *args, **kwargs)
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"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
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

Expand All @@ -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()
Expand All @@ -83,7 +82,7 @@ def hyperopt_loss_function(
# 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

from pandas import DataFrame

from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss

Expand All @@ -21,12 +20,14 @@

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:
Expand Down
5 changes: 1 addition & 4 deletions freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

from pandas import DataFrame

from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_sharpe
from freqtrade.optimize.hyperopt import IHyperOptLoss

Expand All @@ -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:
Expand All @@ -36,7 +34,6 @@ def hyperopt_loss_function(
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
5 changes: 1 addition & 4 deletions freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

from pandas import DataFrame

from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_sortino
from freqtrade.optimize.hyperopt import IHyperOptLoss

Expand All @@ -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:
Expand All @@ -36,7 +34,6 @@ def hyperopt_loss_function(
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
Loading

0 comments on commit a85f200

Please sign in to comment.