Merge branch 'develop' into add-custom-roi-strategy-callback

This commit is contained in:
Matthias
2025-05-15 07:22:25 +02:00
113 changed files with 16227 additions and 8154 deletions

View File

@@ -9,14 +9,18 @@ from collections import defaultdict
from copy import deepcopy
from datetime import datetime, timedelta
from numpy import nan
from pandas import DataFrame
from numpy import isnan, nan
from pandas import DataFrame, Series
from freqtrade import constants
from freqtrade.configuration import TimeRange, validate_config_consistency
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, IntOrInf, LongShort
from freqtrade.data import history
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
from freqtrade.data.btanalysis import (
find_existing_backtest_stats,
get_tick_size_over_time,
trade_list_to_dataframe,
)
from freqtrade.data.converter import trim_dataframe, trim_dataframes
from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.metrics import combined_dataframes_with_rel_mean
@@ -35,7 +39,7 @@ from freqtrade.exchange import (
price_to_precision,
timeframe_to_seconds,
)
from freqtrade.exchange.exchange import Exchange
from freqtrade.exchange.exchange import TICK_SIZE, Exchange
from freqtrade.ft_types import (
BacktestContentType,
BacktestContentTypeIcomplete,
@@ -121,9 +125,10 @@ class Backtesting:
self.order_id_counter: int = 0
config["dry_run"] = True
self.price_pair_prec: dict[str, Series] = {}
self.run_ids: dict[str, str] = {}
self.strategylist: list[IStrategy] = []
self.all_results: dict[str, BacktestContentType] = {}
self.all_bt_content: dict[str, BacktestContentType] = {}
self.analysis_results: dict[str, dict[str, DataFrame]] = {
"signals": {},
"rejected": {},
@@ -315,9 +320,15 @@ class Backtesting:
)
self.progress.set_new_value(1)
self._load_bt_data_detail()
self.price_pair_prec = {}
for pair in self.pairlists.whitelist:
if pair in data:
# Load price precision logic
self.price_pair_prec[pair] = get_tick_size_over_time(data[pair])
return data, self.timerange
def load_bt_data_detail(self) -> None:
def _load_bt_data_detail(self) -> None:
"""
Loads backtest detail data (smaller timeframe) if necessary.
"""
@@ -384,6 +395,22 @@ class Backtesting:
else:
self.futures_data = {}
def get_pair_precision(self, pair: str, current_time: datetime) -> tuple[float | None, int]:
"""
Get pair precision at that moment in time
:param pair: Pair to get precision for
:param current_time: Time to get precision for
:return: tuple of price precision, precision_mode_price for the pair at that given time.
"""
precision_series = self.price_pair_prec.get(pair)
if precision_series is not None:
precision = precision_series.asof(current_time)
if not isnan(precision):
# Force tick size if we define the precision
return precision, TICK_SIZE
return self.exchange.get_precision_price(pair), self.precision_mode_price
def disable_database_use(self):
disable_database_use(self.timeframe)
@@ -806,7 +833,7 @@ class Backtesting:
)
if rate is not None and rate != close_rate:
close_rate = price_to_precision(
rate, trade.price_precision, self.precision_mode_price
rate, trade.price_precision, trade.precision_mode_price
)
# We can't place orders lower than current low.
# freqtrade does not support this in live, and the order would fill immediately
@@ -939,6 +966,7 @@ class Backtesting:
trade: LocalTrade | None,
order_type: str,
price_precision: float | None,
precision_mode_price: int,
) -> tuple[float, float, float, float]:
if order_type == "limit":
new_rate = strategy_safe_wrapper(
@@ -954,9 +982,7 @@ class Backtesting:
# We can't place orders higher than current high (otherwise it'd be a stop limit entry)
# which freqtrade does not support in live.
if new_rate is not None and new_rate != propose_rate:
propose_rate = price_to_precision(
new_rate, price_precision, self.precision_mode_price
)
propose_rate = price_to_precision(new_rate, price_precision, precision_mode_price)
if direction == "short":
propose_rate = max(propose_rate, row[LOW_IDX])
else:
@@ -1049,7 +1075,7 @@ class Backtesting:
pos_adjust = trade is not None and requested_rate is None
stake_amount_ = stake_amount or (trade.stake_amount if trade else 0.0)
precision_price = self.exchange.get_precision_price(pair)
precision_price, precision_mode_price = self.get_pair_precision(pair, current_time)
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
pair,
@@ -1062,6 +1088,7 @@ class Backtesting:
trade,
order_type,
precision_price,
precision_mode_price,
)
# replace proposed rate if another rate was requested
@@ -1137,7 +1164,7 @@ class Backtesting:
amount_precision=precision_amount,
price_precision=precision_price,
precision_mode=self.precision_mode,
precision_mode_price=self.precision_mode_price,
precision_mode_price=precision_mode_price,
contract_size=contract_size,
orders=[],
)
@@ -1731,7 +1758,7 @@ class Backtesting:
"backtest_end_time": int(backtest_end_time.timestamp()),
}
)
self.all_results[strategy_name] = results
self.all_bt_content[strategy_name] = results
if (
self.config.get("export", "none") == "signals"
@@ -1781,7 +1808,6 @@ class Backtesting:
data: dict[str, DataFrame] = {}
data, timerange = self.load_bt_data()
self.load_bt_data_detail()
logger.info("Dataload complete. Calculating indicators")
self.load_prior_backtest()
@@ -1794,9 +1820,9 @@ class Backtesting:
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
# Update old results with new ones.
if len(self.all_results) > 0:
if len(self.all_bt_content) > 0:
results = generate_backtest_stats(
data, self.all_results, min_date=min_date, max_date=max_date
data, self.all_bt_content, min_date=min_date, max_date=max_date
)
if self.results:
self.results["metadata"].update(results["metadata"])