From 5a6b43da46c1fdd74d6c142c070c950d43c079c2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 May 2025 20:37:57 +0200 Subject: [PATCH] feat: use hsitoric price precision for improved accuracy closes #11203 --- freqtrade/optimize/backtesting.py | 45 +++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cbc77604b..d1645e05b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -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_significant_digits_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,6 +125,7 @@ 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_bt_content: dict[str, BacktestContentType] = {} @@ -189,7 +194,6 @@ class Backtesting: self.fee = max(fee for fee in fees if fee is not None) logger.info(f"Using fee {self.fee:.4%} - worst case fee from exchange (lowest tier).") self.precision_mode = self.exchange.precisionMode - self.precision_mode_price = self.exchange.precision_mode_price if self.config.get("freqai_backtest_live_models", False): from freqtrade.freqai.utils import get_timerange_backtest_live_models @@ -316,6 +320,9 @@ class Backtesting: self.progress.set_new_value(1) self._load_bt_data_detail() + self.price_pair_prec = {} + for pair in self.pairlists.whitelist: + self.price_pair_prec[pair] = get_significant_digits_over_time(data[pair]) return data, self.timerange def _load_bt_data_detail(self) -> None: @@ -385,6 +392,22 @@ class Backtesting: else: self.futures_data = {} + def get_pair_precision(self, pair: str, current_time: datetime) -> tuple[float, 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.exchange.precision_mode_price + def disable_database_use(self): disable_database_use(self.timeframe) @@ -793,7 +816,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 @@ -926,6 +949,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( @@ -941,9 +965,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: @@ -1036,7 +1058,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, @@ -1049,6 +1071,7 @@ class Backtesting: trade, order_type, precision_price, + precision_mode_price, ) # replace proposed rate if another rate was requested @@ -1124,7 +1147,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=[], )