Merge branch 'freqtrade:develop' into main-stash

This commit is contained in:
Stefano
2026-02-27 16:53:10 +09:00
committed by GitHub
12 changed files with 266 additions and 85 deletions

View File

@@ -1,4 +1,4 @@
FROM python:3.13.11-slim-trixie AS base
FROM python:3.13.12-slim-trixie AS base
# Setup env
ENV LANG=C.UTF-8

View File

@@ -4,6 +4,7 @@
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
[![codecov](https://codecov.io/gh/freqtrade/freqtrade/branch/develop/graph/badge.svg?token=AD5BG3ATKI)](https://codecov.io/gh/freqtrade/freqtrade)
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
[![Discord Server](https://img.shields.io/badge/Freqtrade_Discord-18181B?logo=discord)](https://discord.gg/p7nuUNVfP7)
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.

View File

@@ -4,7 +4,7 @@
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
[![codecov](https://codecov.io/gh/freqtrade/freqtrade/branch/develop/graph/badge.svg?token=AD5BG3ATKI)](https://codecov.io/gh/freqtrade/freqtrade)
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
[![Discord Server](https://img.shields.io/badge/Freqtrade_Discord-18181B?logo=discord)](https://discord.gg/p7nuUNVfP7)
<!-- GitHub action buttons -->
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }

View File

@@ -240,3 +240,6 @@ IntOrInf = float
EntryExecuteMode = Literal["initial", "pos_adjust", "replace"]
# Prefixes for low-priced coins like 1000PEPE/USDDT:USDT or KPEPE/USDC (hyperliquid)
PairPrefixes = ["1000", "1000000", "1M", "K"]

View File

@@ -1,3 +1,4 @@
from numpy import format_float_positional
from pandas import DataFrame, Series
@@ -11,7 +12,10 @@ def get_tick_size_over_time(candles: DataFrame) -> Series:
# count the number of significant digits for the open and close prices
for col in ["open", "high", "low", "close"]:
candles[f"{col}_count"] = (
candles[col].round(14).apply("{:.15f}".format).str.extract(r"\.(\d*[1-9])")[0].str.len()
candles[col]
.apply(format_float_positional, precision=14, unique=False, fractional=False, trim="-")
.str.extract(r"\.(\d*[1-9])")[0]
.str.len()
)
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
axis=1

View File

@@ -70,28 +70,6 @@ class IDataHandler(ABC):
if match and len(match.groups()) > 1
]
@classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> list[str]:
"""
Returns a list of all pairs with ohlcv data available in this datadir
for the specified timeframe
:param datadir: Directory to search for ohlcv files
:param timeframe: Timeframe to search pairs for
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: List of Pairs
"""
candle = ""
if candle_type != CandleType.SPOT:
datadir = datadir.joinpath("futures")
candle = f"-{candle_type}"
ext = cls._get_file_extension()
_tmp = [
re.search(r"^(\S+)(?=\-" + timeframe + candle + f".{ext})", p.name)
for p in datadir.glob(f"*{timeframe}{candle}.{ext}")
]
# Check if regex found something and only return these results
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
@abstractmethod
def ohlcv_store(
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType

View File

@@ -62743,15 +62743,15 @@
"symbol": "OM/USDT:USDT",
"currency": "USDT",
"minNotional": 0.0,
"maxNotional": 2500.0,
"maintenanceMarginRate": 0.025,
"maxNotional": 2000.0,
"maintenanceMarginRate": 0.03,
"maxLeverage": 20.0,
"info": {
"bracket": 1,
"initialLeverage": 20,
"notionalCap": 2500,
"notionalCap": 2000,
"notionalFloor": 0,
"maintMarginRatio": 0.025,
"maintMarginRatio": 0.03,
"cum": 0.0
}
},
@@ -62759,24 +62759,24 @@
"tier": 2.0,
"symbol": "OM/USDT:USDT",
"currency": "USDT",
"minNotional": 2500.0,
"maxNotional": 20000.0,
"minNotional": 2000.0,
"maxNotional": 15000.0,
"maintenanceMarginRate": 0.05,
"maxLeverage": 10.0,
"info": {
"bracket": 2,
"initialLeverage": 10,
"notionalCap": 20000,
"notionalFloor": 2500,
"notionalCap": 15000,
"notionalFloor": 2000,
"maintMarginRatio": 0.05,
"cum": 62.5
"cum": 40.0
}
},
{
"tier": 3.0,
"symbol": "OM/USDT:USDT",
"currency": "USDT",
"minNotional": 20000.0,
"minNotional": 15000.0,
"maxNotional": 60000.0,
"maintenanceMarginRate": 0.1,
"maxLeverage": 5.0,
@@ -62784,9 +62784,9 @@
"bracket": 3,
"initialLeverage": 5,
"notionalCap": 60000,
"notionalFloor": 20000,
"notionalFloor": 15000,
"maintMarginRatio": 0.1,
"cum": 1062.5
"cum": 790.0
}
},
{
@@ -62803,7 +62803,7 @@
"notionalCap": 150000,
"notionalFloor": 60000,
"maintMarginRatio": 0.125,
"cum": 2562.5
"cum": 2290.0
}
},
{
@@ -62820,7 +62820,7 @@
"notionalCap": 300000,
"notionalFloor": 150000,
"maintMarginRatio": 0.1667,
"cum": 8817.5
"cum": 8545.0
}
},
{
@@ -62837,7 +62837,7 @@
"notionalCap": 1000000,
"notionalFloor": 300000,
"maintMarginRatio": 0.25,
"cum": 33807.5
"cum": 33535.0
}
},
{
@@ -62854,7 +62854,7 @@
"notionalCap": 1500000,
"notionalFloor": 1000000,
"maintMarginRatio": 0.5,
"cum": 283807.5
"cum": 283535.0
}
}
],
@@ -64287,6 +64287,144 @@
}
}
],
"OPN/USDT:USDT": [
{
"tier": 1.0,
"symbol": "OPN/USDT:USDT",
"currency": "USDT",
"minNotional": 0.0,
"maxNotional": 5000.0,
"maintenanceMarginRate": 0.02,
"maxLeverage": 25.0,
"info": {
"bracket": 1,
"initialLeverage": 25,
"notionalCap": 5000,
"notionalFloor": 0,
"maintMarginRatio": 0.02,
"cum": 0.0
}
},
{
"tier": 2.0,
"symbol": "OPN/USDT:USDT",
"currency": "USDT",
"minNotional": 5000.0,
"maxNotional": 10000.0,
"maintenanceMarginRate": 0.025,
"maxLeverage": 20.0,
"info": {
"bracket": 2,
"initialLeverage": 20,
"notionalCap": 10000,
"notionalFloor": 5000,
"maintMarginRatio": 0.025,
"cum": 25.0
}
},
{
"tier": 3.0,
"symbol": "OPN/USDT:USDT",
"currency": "USDT",
"minNotional": 10000.0,
"maxNotional": 25000.0,
"maintenanceMarginRate": 0.05,
"maxLeverage": 10.0,
"info": {
"bracket": 3,
"initialLeverage": 10,
"notionalCap": 25000,
"notionalFloor": 10000,
"maintMarginRatio": 0.05,
"cum": 275.0
}
},
{
"tier": 4.0,
"symbol": "OPN/USDT:USDT",
"currency": "USDT",
"minNotional": 25000.0,
"maxNotional": 50000.0,
"maintenanceMarginRate": 0.1,
"maxLeverage": 5.0,
"info": {
"bracket": 4,
"initialLeverage": 5,
"notionalCap": 50000,
"notionalFloor": 25000,
"maintMarginRatio": 0.1,
"cum": 1525.0
}
},
{
"tier": 5.0,
"symbol": "OPN/USDT:USDT",
"currency": "USDT",
"minNotional": 50000.0,
"maxNotional": 100000.0,
"maintenanceMarginRate": 0.125,
"maxLeverage": 4.0,
"info": {
"bracket": 5,
"initialLeverage": 4,
"notionalCap": 100000,
"notionalFloor": 50000,
"maintMarginRatio": 0.125,
"cum": 2775.0
}
},
{
"tier": 6.0,
"symbol": "OPN/USDT:USDT",
"currency": "USDT",
"minNotional": 100000.0,
"maxNotional": 250000.0,
"maintenanceMarginRate": 0.1667,
"maxLeverage": 3.0,
"info": {
"bracket": 6,
"initialLeverage": 3,
"notionalCap": 250000,
"notionalFloor": 100000,
"maintMarginRatio": 0.1667,
"cum": 6945.0
}
},
{
"tier": 7.0,
"symbol": "OPN/USDT:USDT",
"currency": "USDT",
"minNotional": 250000.0,
"maxNotional": 500000.0,
"maintenanceMarginRate": 0.25,
"maxLeverage": 2.0,
"info": {
"bracket": 7,
"initialLeverage": 2,
"notionalCap": 500000,
"notionalFloor": 250000,
"maintMarginRatio": 0.25,
"cum": 27770.0
}
},
{
"tier": 8.0,
"symbol": "OPN/USDT:USDT",
"currency": "USDT",
"minNotional": 500000.0,
"maxNotional": 800000.0,
"maintenanceMarginRate": 0.5,
"maxLeverage": 1.0,
"info": {
"bracket": 8,
"initialLeverage": 1,
"notionalCap": 800000,
"notionalFloor": 500000,
"maintMarginRatio": 0.5,
"cum": 152770.0
}
}
],
"ORBS/USDT:USDT": [
{
"tier": 1.0,

View File

@@ -86,7 +86,7 @@ class PairLocks:
lock
for lock in PairLocks.locks
if (
lock.lock_end_time >= now
lock.lock_end_time > now
and lock.active is True
and (pair is None or lock.pair == pair)
and (side is None or lock.side == "*" or lock.side == side)

View File

@@ -7,6 +7,7 @@ Provides dynamic pair list based on Market Cap
import logging
import math
from freqtrade.constants import PairPrefixes
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.exchange_types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
@@ -162,9 +163,6 @@ class MarketCapPairList(IPairList):
return pairlist
# Prefixes to test to discover coins like 1000PEPE/USDDT:USDT or KPEPE/USDC (hyperliquid)
prefixes = ("1000", "K")
def resolve_marketcap_pair(
self,
pair: str,
@@ -179,7 +177,7 @@ class MarketCapPairList(IPairList):
return pair
if pair not in markets:
for prefix in self.prefixes:
for prefix in PairPrefixes:
test_prefix = f"{prefix}{pair}"
if test_prefix in pairlist:

View File

@@ -24,36 +24,6 @@ from freqtrade.exceptions import OperationalException
from tests.conftest import log_has, log_has_re
def test_datahandler_ohlcv_get_pairs(testdatadir):
pairs = FeatherDataHandler.ohlcv_get_pairs(testdatadir, "5m", candle_type=CandleType.SPOT)
# Convert to set to avoid failures due to sorting
assert set(pairs) == {
"UNITTEST/BTC",
"XLM/BTC",
"ETH/BTC",
"TRX/BTC",
"LTC/BTC",
"XMR/BTC",
"ZEC/BTC",
"ADA/BTC",
"ETC/BTC",
"NXT/BTC",
"DASH/BTC",
"XRP/ETH",
"BTC/USDT",
"XRP/USDT",
}
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, "8m", candle_type=CandleType.SPOT)
assert set(pairs) == {"UNITTEST/BTC"}
pairs = FeatherDataHandler.ohlcv_get_pairs(testdatadir, "1h", candle_type=CandleType.MARK)
assert set(pairs) == {"UNITTEST/USDT:USDT", "XRP/USDT:USDT"}
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, "1h", candle_type=CandleType.FUTURES)
assert set(pairs) == {"XRP/USDT:USDT"}
@pytest.mark.parametrize(
"filename,pair,timeframe,candletype",
[

View File

@@ -172,7 +172,6 @@ def test_get_tick_size_over_time_small_numbers():
# January should have 5 significant digits (based on 1.23456789 being the most precise value)
# which should be converted to 0.00001
assert result.asof("2020-01-01 00:00:00+00:00") == 0.000000000001
assert result.asof("2020-01-01 00:00:00+00:00") == 0.000000000001
assert result.asof("2020-02-25 00:00:00+00:00") == 0.00000000001
assert result.asof("2020-03-25 00:00:00+00:00") == 0.000000001
@@ -181,3 +180,93 @@ def test_get_tick_size_over_time_small_numbers():
assert result.asof("2025-04-01 00:00:00+00:00") == 0.000000001
assert result.iloc[0] == 0.000000000001
def test_get_tick_size_over_time_big_numbers():
"""
Test the get_tick_size_over_time function with predefined data
"""
# Create test dataframe with different levels of precision
data = {
"date": [
Timestamp("2020-01-01 00:00:00", tz=UTC),
Timestamp("2020-01-02 00:00:00", tz=UTC),
Timestamp("2020-01-03 00:00:00", tz=UTC),
Timestamp("2020-01-15 00:00:00", tz=UTC),
Timestamp("2020-01-16 00:00:00", tz=UTC),
Timestamp("2020-01-31 00:00:00", tz=UTC),
Timestamp("2020-02-01 00:00:00", tz=UTC),
Timestamp("2020-02-15 00:00:00", tz=UTC),
Timestamp("2020-03-15 00:00:00", tz=UTC),
],
"open": [
12345.123456,
12345.1234,
12345.123,
12345.12,
12345.123456,
12345.1234,
12345.23456,
12345,
12345.234,
],
"high": [
12345.123457,
12345.1235,
12345.124,
12345.13,
12345.123456,
12345.1235,
12345.23457,
12345,
12345.234,
],
"low": [
12345.123455,
12345.1233,
12345.122,
12345.11,
12345.123456,
12345.1233,
12345.23455,
12345,
12345.234,
],
"close": [
12345.123456,
12345.1234,
12345.123,
12345.12,
12345.123456,
12345.1234,
12345.23456,
12345,
12345.234,
],
"volume": [100, 200, 300, 400, 500, 600, 700, 800, 900],
}
candles = DataFrame(data)
# Calculate significant digits
result = get_tick_size_over_time(candles)
# Check that the result is a pandas Series
assert isinstance(result, pd.Series)
# Check that we have three months of data (Jan, Feb and March 2020 )
assert len(result) == 3
# Before
assert result.asof("2019-01-01 00:00:00+00:00") is nan
# January should have 5 significant digits (based on 1.23456789 being the most precise value)
# which should be converted to 0.00001
assert result.asof("2020-01-01 00:00:00+00:00") == 0.000001
assert result.asof("2020-02-25 00:00:00+00:00") == 0.00001
assert result.asof("2020-03-25 00:00:00+00:00") == 0.001
assert result.asof("2020-04-01 00:00:00+00:00") == 0.001
# Value far past the last date should be the last value
assert result.asof("2025-04-01 00:00:00+00:00") == 0.001
assert result.iloc[0] == 0.000001

View File

@@ -1346,11 +1346,11 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
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"))
tests = [
["sine", 9],
["raise", 10],
["sine", 10],
["raise", 11],
["lower", 0],
["sine", 9],
["raise", 10],
["sine", 10],
["raise", 11],
]
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
@@ -1380,11 +1380,11 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
(None, "lower", 0),
(None, "sine", 35),
(None, "raise", 19),
([{"method": "CooldownPeriod", "stop_duration": 3}], "sine", 9),
([{"method": "CooldownPeriod", "stop_duration": 3}], "raise", 10),
([{"method": "CooldownPeriod", "stop_duration": 3}], "sine", 10),
([{"method": "CooldownPeriod", "stop_duration": 3}], "raise", 11),
([{"method": "CooldownPeriod", "stop_duration": 3}], "lower", 0),
([{"method": "CooldownPeriod", "stop_duration": 3}], "sine", 9),
([{"method": "CooldownPeriod", "stop_duration": 3}], "raise", 10),
([{"method": "CooldownPeriod", "stop_duration": 3}], "sine", 10),
([{"method": "CooldownPeriod", "stop_duration": 3}], "raise", 11),
],
)
def test_backtest_pricecontours(