Merge branch 'develop' into fix/tz-naive-predictions

This commit is contained in:
Matthias
2025-08-07 06:35:35 +02:00
54 changed files with 5097 additions and 2023 deletions

View File

@@ -29,6 +29,10 @@ updates:
mkdocs:
patterns:
- "mkdocs*"
scipy:
patterns:
- "scipy"
- "scipy-stubs"
- package-ecosystem: "github-actions"
directory: "/"

View File

@@ -38,7 +38,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true
@@ -174,7 +174,7 @@ jobs:
check-latest: true
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true
@@ -301,7 +301,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true
@@ -449,7 +449,7 @@ jobs:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true

View File

@@ -28,7 +28,7 @@ jobs:
with:
persist-credentials: false
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@@ -21,18 +21,18 @@ repos:
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.16.1"
rev: "v1.17.1"
hooks:
- id: mypy
exclude: build_helpers
additional_dependencies:
- types-cachetools==6.0.0.20250525
- types-cachetools==6.1.0.20250717
- types-filelock==3.2.7
- types-requests==2.32.4.20250611
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20250708
- scipy-stubs==1.16.0.2
- SQLAlchemy==2.0.41
- scipy-stubs==1.16.1.0
- SQLAlchemy==2.0.42
# stages: [push]
- repo: https://github.com/pycqa/isort
@@ -44,7 +44,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.12.3'
rev: 'v0.12.7'
hooks:
- id: ruff
- id: ruff-format

View File

@@ -180,6 +180,16 @@
"description": "Offset for profit exit. \nUsually specified in the strategy and missing in the configuration.",
"type": "number"
},
"recursive_strategy_search": {
"description": "Enable recursive strategy search.",
"type": "boolean"
},
"user_data_dir": {
"description": "Path to the user data directory."
},
"datadir": {
"description": "Path to the data directory."
},
"fee": {
"description": "Trading fee percentage. Can help to simulate slippage in backtesting",
"type": "number",

View File

@@ -1,10 +0,0 @@
FROM freqtradeorg/freqtrade:develop
# Install dependencies
COPY requirements-dev.txt /freqtrade/
RUN pip install numpy --user --no-cache-dir \
&& pip install -r requirements-dev.txt --user --no-cache-dir
# Empty the ENTRYPOINT to allow all commands
ENTRYPOINT []

View File

@@ -321,6 +321,7 @@ It contains some useful key metrics about performance of your strategy on backte
| SQN | 2.45 |
| Profit factor | 1.11 |
| Expectancy (Ratio) | -0.15 (-0.05) |
| Avg. daily profit | 0.0001 BTC |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
@@ -374,6 +375,8 @@ It contains some useful key metrics about performance of your strategy on backte
- `Calmar`: Annualized Calmar ratio.
- `SQN`: System Quality Number (SQN) - by Van Tharp.
- `Profit factor`: profit / loss.
- `Expectancy (Ratio)`: Expectancy ratio, which is the average profit or loss per trade. A negative expectancy ratio means that your strategy is not profitable.
- `Avg. daily profit`: Average profit per day, calculated as `(Total Profit / Backtest Days)`.
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `Best Pair` / `Worst Pair`: Best and worst performing pair (based on absolute profit), and it's corresponding `Tot Profit %`.

View File

@@ -1,11 +1,16 @@
```
usage: freqtrade list-exchanges [-h] [-v] [--no-color] [--logfile FILE] [-V]
[-c PATH] [-d PATH] [--userdir PATH] [-1] [-a]
[--trading-mode {spot,margin,futures}]
[--dex-exchanges]
options:
-h, --help show this help message and exit
-1, --one-column Print output in one column.
-a, --all Print all exchanges known to the ccxt library.
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
Select Trading mode
--dex-exchanges Print only DEX exchanges.
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@@ -328,6 +328,22 @@ It's therefore required to pass the UID as well.
!!! Warning "Necessary Verification"
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
## Bitget
Bitget requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
```json
"exchange": {
"name": "bitget",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"password": "your_exchange_api_key_password",
// ...
}
```
Bitget supports [time_in_force](configuration.md#understand-order_time_in_force).
## Hyperliquid
!!! Tip "Stoploss on Exchange"
@@ -339,13 +355,13 @@ This needs to be configured like this:
```json
"exchange": {
"name": "hyperliquid",
"walletAddress": "your_eth_wallet_address",
"walletAddress": "your_eth_wallet_address", // This should NOT be your API Wallet Address!
"privateKey": "your_api_private_key",
// ...
}
```
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your wallet address, not your API Wallet Address.
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your main wallet address, not your API Wallet Address.
* privateKey in hex format: `0x<64 hex characters>` - Use the key the API Wallet shows on creation.
Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed.
@@ -363,6 +379,27 @@ Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid.
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
### Hyperliquid Vault / Subaccount
Hyperliquid allows you to create either a vault or a subaccount.
To use these with Freqtrade, you will need to use the following configuration pattern:
``` json
"exchange": {
"name": "hyperliquid",
"walletAddress": "your_vault_address", // Vault or subaccount address
"privateKey": "your_api_private_key",
"ccxt_config": {
"options": {
"vaultAddress": "your_vault_address" // Optional, only if you want to use a vault or subaccount
}
},
// ...
}
```
Your balance and trades will now be used from your vault / subaccount - and no longer from your main account.
### Historic Hyperliquid data
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.

View File

@@ -1,7 +1,7 @@
markdown==3.8.2
mkdocs==1.6.1
mkdocs-material==9.6.15
mkdocs-material==9.6.16
mdx_truly_sane_lists==1.3
pymdown-extensions==10.16
pymdown-extensions==10.16.1
jinja2==3.1.6
mike==2.1.3

View File

@@ -117,9 +117,9 @@ Different payloads can be configured for different events. Not all fields are ne
## Webhook Message types
### Entry
### Entry / Entry fill
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format.
The fields in `webhook.entry` and `webhook.entry_fill` are filled when the bot places a long/short Order to increase a position, or when that order fills respectively. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
@@ -162,31 +162,9 @@ Possible parameters are:
* `current_rate`
* `enter_tag`
### Entry fill
### Exit / Exit fill
The fields in `webhook.entry_fill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `open_rate`
* `amount`
* `open_date`
* `stake_amount`
* `stake_currency`
* `base_currency`
* `quote_currency`
* `fiat_currency`
* `order_type`
* `current_rate`
* `enter_tag`
### Exit
The fields in `webhook.exit` are filled when the bot exits a trade. Parameters are filled using string.format.
The fields in `webhook.exit` and `webhook.exit_fill` are filled when the bot places an exit order, or when that exit order fills respectively. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
@@ -195,34 +173,9 @@ Possible parameters are:
* `direction`
* `leverage`
* `gain`
* `limit`
* `amount`
* `open_rate`
* `profit_amount`
* `profit_ratio`
* `stake_currency`
* `base_currency`
* `quote_currency`
* `fiat_currency`
* `exit_reason`
* `order_type`
* `open_date`
* `close_date`
### Exit fill
The fields in `webhook.exit_fill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `gain`
* `close_rate`
* `amount`
* `open_rate`
* `current_rate`
* `profit_amount`
* `profit_ratio`
@@ -230,10 +183,14 @@ Possible parameters are:
* `base_currency`
* `quote_currency`
* `fiat_currency`
* `enter_tag`
* `exit_reason`
* `order_type`
* `open_date`
* `close_date`
* `sub_trade`
* `is_final_exit`
### Exit cancel
@@ -246,7 +203,7 @@ Possible parameters are:
* `direction`
* `leverage`
* `gain`
* `limit`
* `order_rate`
* `amount`
* `open_rate`
* `current_rate`

View File

@@ -1,6 +1,6 @@
"""Freqtrade bot"""
__version__ = "2025.7-dev"
__version__ = "2025.8-dev"
if "dev" in __version__:
from pathlib import Path

View File

@@ -96,7 +96,7 @@ ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"]
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"]
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
@@ -258,24 +258,26 @@ ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs"
# Command level configs - keep at the bottom of the above definitions
NO_CONF_REQURIED = [
"backtest-filter",
"backtesting-show",
"convert-data",
"convert-trade-data",
"download-data",
"list-timeframes",
"hyperopt-list",
"hyperopt-show",
"list-data",
"list-freqaimodels",
"list-hyperoptloss",
"list-markets",
"list-pairs",
"list-strategies",
"list-freqaimodels",
"list-hyperoptloss",
"list-data",
"hyperopt-list",
"hyperopt-show",
"backtest-filter",
"list-timeframes",
"plot-dataframe",
"plot-profit",
"show-trades",
"trades-to-ohlcv",
"install-ui",
"strategy-updater",
"trades-to-ohlcv",
]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
@@ -311,8 +313,6 @@ class Arguments:
# (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting)
if "config" in parsed_arg and parsed_arg.config is None:
conf_required = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None:
user_dir = parsed_arg.user_data_dir
else:
@@ -325,7 +325,9 @@ class Arguments:
else:
# Else use "config.json".
cfgfile = Path.cwd() / DEFAULT_CONFIG
if cfgfile.is_file() or not conf_required:
conf_optional = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
if cfgfile.is_file() or not conf_optional:
# Only inject config if the file exists, or if the config is required
parsed_arg.config = [DEFAULT_CONFIG]
return parsed_arg

View File

@@ -369,6 +369,11 @@ AVAILABLE_CLI_OPTIONS = {
help="Print all exchanges known to the ccxt library.",
action="store_true",
),
"dex_exchanges": Arg(
"--dex-exchanges",
help="Print only DEX exchanges.",
action="store_true",
),
# List pairs / markets
"list_pairs_all": Arg(
"-a",

View File

@@ -46,7 +46,18 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
table.add_column("Markets")
table.add_column("Reason")
trading_mode = args.get("trading_mode", None)
dex_only = args.get("dex_exchanges", False)
for exchange in available_exchanges:
if trading_mode and not any(
a["trading_mode"] == trading_mode for a in exchange["trade_modes"]
):
# If trading_mode is specified, only show exchanges that support it
continue
if dex_only and not exchange.get("dex", False):
# If dex_only is specified, only show DEX exchanges
continue
name = Text(exchange["name"])
if exchange["supported"]:
name.append(" (Supported)", style="italic")

View File

@@ -157,6 +157,16 @@ CONF_SCHEMA = {
"description": f"Offset for profit exit. {__IN_STRATEGY}",
"type": "number",
},
"recursive_strategy_search": {
"description": "Enable recursive strategy search.",
"type": "boolean",
},
"user_data_dir": {
"description": "Path to the user data directory.",
},
"datadir": {
"description": "Path to the data directory.",
},
"fee": {
"description": "Trading fee percentage. Can help to simulate slippage in backtesting",
"type": "number",

View File

@@ -11,7 +11,7 @@ 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).astype(str).str.extract(r"\.(\d*[1-9])")[0].str.len()
candles[col].round(14).apply("{:.15f}".format).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

@@ -6,6 +6,7 @@ from freqtrade.exchange.exchange import Exchange
# isort: on
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bingx import Bingx
from freqtrade.exchange.bitget import Bitget
from freqtrade.exchange.bitmart import Bitmart
from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bitvavo import Bitvavo
@@ -45,4 +46,4 @@ from freqtrade.exchange.kucoin import Kucoin
from freqtrade.exchange.lbank import Lbank
from freqtrade.exchange.luno import Luno
from freqtrade.exchange.modetrade import Modetrade
from freqtrade.exchange.okx import Okx
from freqtrade.exchange.okx import MyOkx, Okx

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
import logging
from datetime import timedelta
from freqtrade.enums import CandleType
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
from freqtrade.util.datetime_helpers import dt_now, dt_ts
logger = logging.getLogger(__name__)
class Bitget(Exchange):
"""
Bitget exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: FtHas = {
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
}
_ft_has_futures: FtHas = {
"mark_ohlcv_timeframe": "4h",
}
def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
) -> int:
"""
Exchange ohlcv candle limit
bitget has the following behaviour:
* 1000 candles for up-to-date data
* 200 candles for historic data (prior to a certain date)
:param timeframe: Timeframe to check
:param candle_type: Candle-type
:param since_ms: Starting timestamp
:return: Candle limit as integer
"""
timeframe_map = self._api.options["fetchOHLCV"]["maxRecentDaysPerTimeframe"]
days = timeframe_map.get(timeframe, 30)
if candle_type in (CandleType.FUTURES, CandleType.SPOT) and (
not since_ms or dt_ts(dt_now() - timedelta(days=days)) < since_ms
):
return 1000
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)

View File

@@ -1,8 +1,5 @@
"""Bybit exchange subclass"""
import logging
from datetime import datetime, timedelta
from typing import Any
import ccxt
@@ -12,6 +9,7 @@ from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalExcep
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.misc import deep_merge_dicts
logger = logging.getLogger(__name__)
@@ -76,14 +74,11 @@ class Bybit(Exchange):
config = {}
if self.trading_mode == TradingMode.SPOT:
config.update({"options": {"defaultType": "spot"}})
config.update(super()._ccxt_config)
elif self.trading_mode == TradingMode.FUTURES:
config.update({"options": {"defaultSettle": self._config["stake_currency"]}})
config = deep_merge_dicts(config, super()._ccxt_config)
return config
def market_is_future(self, market: dict[str, Any]) -> bool:
main = super().market_is_future(market)
# For ByBit, we'll only support USDT markets for now.
return main and market["settle"] == "USDT"
@retrier
def additional_exchange_init(self) -> None:
"""
@@ -182,18 +177,36 @@ class Bybit(Exchange):
PERPETUAL:
bybit:
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
USDT:
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
USDC:
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#c
Long:
Long USDT:
Liquidation Price = (
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
- (Extra Margin Added/Contract Quantity))
Short USDT:
Liquidation Price = (
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
+ (Extra Margin Added/Contract Quantity))
Long USDC:
Liquidation Price = (
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
- (Extra Margin Added/Contract Quantity))
Short:
Position Entry Price - [
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
]
)
Short USDC:
Liquidation Price = (
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
+ (Extra Margin Added/Contract Quantity))
Position Entry Price + [
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
]
)
Implementation Note: Extra margin is currently not used.
Due to this - the liquidation formula between USDT and USDC is the same.
:param pair: Pair to calculate liquidation price for
:param open_rate: Entry price of position

View File

@@ -46,10 +46,9 @@ BAD_EXCHANGES = {
MAP_EXCHANGE_CHILDCLASS = {
"binanceus": "binance",
"binanceje": "binance",
"binanceusdm": "binance",
"okex": "okx",
"myokx": "okx",
"okxus": "okx",
"gateio": "gate",
"huboi": "htx",
}
@@ -64,6 +63,7 @@ SUPPORTED_EXCHANGES = [
"hyperliquid",
"kraken",
"okx",
"myokx",
]
# either the main, or replacement methods (array) is required

View File

@@ -294,10 +294,6 @@ class Exchange:
# Initial markets load
self.reload_markets(True, load_leverage_tiers=False)
self.validate_config(config)
self._startup_candle_count: int = config.get("startup_candle_count", 0)
self.required_candle_call_count = self.validate_required_startup_candles(
self._startup_candle_count, config.get("timeframe", "")
)
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
self.fill_leverage_tiers()
@@ -336,6 +332,12 @@ class Exchange:
asyncio.set_event_loop(loop)
return loop
def _set_startup_candle_count(self, config: Config) -> None:
self._startup_candle_count: int = config.get("startup_candle_count", 0)
self.required_candle_call_count = self.validate_required_startup_candles(
self._startup_candle_count, config.get("timeframe", "")
)
def validate_config(self, config: Config) -> None:
# Check if timeframe is available
self.validate_timeframes(config.get("timeframe"))
@@ -350,6 +352,8 @@ class Exchange:
self.validate_orderflow(config["exchange"])
self.validate_freqai(config)
self._set_startup_candle_count(config)
def _init_ccxt(
self, exchange_config: dict[str, Any], sync: bool, ccxt_kwargs: dict[str, Any]
) -> ccxt.Exchange:

View File

@@ -213,9 +213,9 @@ def amount_to_precision(
amount = float(
decimal_to_precision(
amount,
rounding_mode=TRUNCATE,
precision=precision,
counting_mode=precisionMode,
TRUNCATE, # rounding_mode
precision, # numPrecisionDigits
precisionMode, # counting_mode
)
)
@@ -311,11 +311,11 @@ def price_to_precision(
return float(
decimal_to_precision(
price,
rounding_mode=rounding_mode,
precision=int(price_precision)
rounding_mode, # rounding mode
int(price_precision)
if precisionMode != TICK_SIZE
else price_precision,
counting_mode=precisionMode,
else price_precision, # numPrecisionDigits
precisionMode, # counting_mode
)
)

View File

@@ -287,3 +287,14 @@ class Okx(Exchange):
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
orders.extend(orders_open)
return orders
class MyOkx(Okx):
"""
MyOkx exchange class.
Minimal adjustment to disable futures trading for the EU subsidiary of Okx
"""
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.SPOT, MarginMode.NONE),
]

View File

@@ -493,7 +493,7 @@ class FreqaiDataDrawer:
dk.data["data_path"] = str(dk.data_path)
dk.data["model_filename"] = str(dk.model_filename)
dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns)
dk.data["training_features_list"] = dk.training_features_list
dk.data["label_list"] = dk.label_list
with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp:

View File

@@ -514,12 +514,7 @@ class IFreqaiModel(ABC):
current coin/bot loop
"""
if "training_features_list_raw" in dk.data:
feature_list = dk.data["training_features_list_raw"]
else:
feature_list = dk.data["training_features_list"]
if dk.training_features_list != feature_list:
if dk.training_features_list != dk.data["training_features_list"]:
raise OperationalException(
"Trying to access pretrained model with `identifier` "
"but found different features furnished by current strategy. "

View File

@@ -93,14 +93,16 @@ class FreqtradeBot(LoggingMixin):
# Remove credentials from original exchange config to avoid accidental credential exposure
remove_exchange_credentials(config["exchange"], True)
self.exchange = ExchangeResolver.load_exchange(
self.config, exchange_config=exchange_config, load_leverage_tiers=True
)
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
# Check config consistency here since strategies can set certain options
validate_config_consistency(config)
self.exchange = ExchangeResolver.load_exchange(
self.config, exchange_config=exchange_config, load_leverage_tiers=True
)
# Re-validate exchange compatibility
self.exchange.validate_config(self.config)
init_db(self.config["db_url"])
@@ -1214,6 +1216,7 @@ class FreqtradeBot(LoggingMixin):
"leverage": trade.leverage if trade.leverage else None,
"direction": "Short" if trade.is_short else "Long",
"limit": open_rate, # Deprecated (?)
"order_rate": open_rate,
"open_rate": open_rate,
"order_type": order_type or "unknown",
"stake_amount": stake_amount,
@@ -1250,6 +1253,7 @@ class FreqtradeBot(LoggingMixin):
"leverage": trade.leverage,
"direction": "Short" if trade.is_short else "Long",
"limit": trade.open_rate,
"order_rate": trade.open_rate,
"order_type": order_type,
"stake_amount": trade.stake_amount,
"open_rate": trade.open_rate,
@@ -2245,6 +2249,7 @@ class FreqtradeBot(LoggingMixin):
"direction": "Short" if trade.is_short else "Long",
"gain": gain,
"limit": profit_rate or 0,
"order_rate": profit_rate or 0,
"order_type": order_type,
"amount": order.safe_amount_after_fee,
"open_rate": trade.open_rate,

View File

@@ -125,6 +125,7 @@ class LookaheadAnalysis(BaseAnalysis):
backtesting = Backtesting(prepare_data_config, self.exchange)
self.exchange = backtesting.exchange
self.local_config["candle_type_def"] = prepare_data_config["candle_type_def"]
self._fee = backtesting.fee
backtesting._set_strategy(backtesting.strategylist[0])

View File

@@ -146,6 +146,7 @@ class RecursiveAnalysis(BaseAnalysis):
backtesting = Backtesting(prepare_data_config, self.exchange)
self.exchange = backtesting.exchange
self.local_config["candle_type_def"] = prepare_data_config["candle_type_def"]
backtesting._set_strategy(backtesting.strategylist[0])
varholder.data, varholder.timerange = backtesting.load_bt_data()

View File

@@ -332,8 +332,11 @@ def text_table_add_metrics(strat_results: dict) -> None:
),
),
(
"Avg. daily profit %",
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}",
"Avg. daily profit",
fmt_coin(
(strat_results["profit_total_abs"] / strat_results["backtest_days"]),
strat_results["stake_currency"],
),
),
(
"Avg. stake amount",

View File

@@ -598,6 +598,8 @@ def generate_strategy_stats(
"timerange": config.get("timerange", ""),
"enable_protections": config.get("enable_protections", False),
"strategy_name": strategy,
"freqaimodel": config.get("freqaimodel", None),
"freqai_identifier": config.get("freqai", {}).get("identifier", None),
# Parameters relevant for backtesting
"stoploss": config["stoploss"],
"trailing_stop": config.get("trailing_stop", False),

View File

@@ -54,7 +54,7 @@ class StrategyResolver(IResolver):
strategy.ft_load_params_from_file()
# Set attributes
# Check if we need to override configuration
# (Attribute name, default, subkey)
# (Attribute name, default, subkey)
attributes = [
("minimal_roi", {"0": 10.0}),
("timeframe", None),

View File

@@ -173,6 +173,12 @@ class Profit(BaseModel):
bot_start_date: str
class ProfitAll(BaseModel):
all: Profit
long: Profit | None = None
short: Profit | None = None
class SellReason(BaseModel):
wins: int
losses: int

View File

@@ -43,6 +43,7 @@ from freqtrade.rpc.api_server.api_schemas import (
Ping,
PlotConfig,
Profit,
ProfitAll,
ResultMsg,
ShowConfig,
Stats,
@@ -89,7 +90,8 @@ logger = logging.getLogger(__name__)
# 2.40: Add hyperopt-loss endpoint
# 2.41: Add download-data endpoint
# 2.42: Add /pair_history endpoint with live data
API_VERSION = 2.42
# 2.43: Add /profit_all endpoint
API_VERSION = 2.43
# Public API, requires no auth.
router_public = APIRouter()
@@ -148,6 +150,24 @@ def profit(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
return rpc._rpc_trade_statistics(config["stake_currency"], config.get("fiat_display_currency"))
@router.get("/profit_all", response_model=ProfitAll, tags=["info"])
def profit_all(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
response = {
"all": rpc._rpc_trade_statistics(
config["stake_currency"], config.get("fiat_display_currency")
),
}
if config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
response["long"] = rpc._rpc_trade_statistics(
config["stake_currency"], config.get("fiat_display_currency"), direction="long"
)
response["short"] = rpc._rpc_trade_statistics(
config["stake_currency"], config.get("fiat_display_currency"), direction="short"
)
return response
@router.get("/stats", response_model=Stats, tags=["info"])
def stats(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_stats()

View File

@@ -56,7 +56,8 @@ class __RPCEntryExitMsgBase(RPCSendMsgBase):
quote_currency: str
leverage: float | None
direction: str
limit: float
limit: float # Deprecated, use order_rate instead
order_rate: float
open_rate: float
order_type: str
stake_amount: float
@@ -87,7 +88,6 @@ class RPCExitMsg(__RPCEntryExitMsgBase):
exit_reason: str | None
close_date: datetime
# current_rate: float | None
order_rate: float | None
final_profit_ratio: float | None
is_final_exit: bool

View File

@@ -39,6 +39,17 @@ class StrategyUpdater:
"sell": "exit",
}
# Update function names.
# example: `np.NaN` was removed in the NumPy 2.0 release. Use `np.nan` instead.
module_replacements = {
"numpy": {
"aliases": set(),
"replacements": [
("NaN", "nan"),
],
}
}
# create a dictionary that maps the old column names to the new ones
rename_dict = {"buy": "enter_long", "sell": "exit_long", "buy_tag": "enter_tag"}
@@ -153,16 +164,24 @@ class NameUpdater(ast_comments.NodeTransformer):
def visit_Name(self, node):
# if the name is in the mapping, update it
node.id = self.check_dict(StrategyUpdater.name_mapping, node.id)
for mod, info in StrategyUpdater.module_replacements.items():
for old_attr, new_attr in info["replacements"]:
if node.id == old_attr:
node.id = new_attr
return node
def visit_Import(self, node):
# do not update the names in import statements
for alias in node.names:
if alias.name in StrategyUpdater.module_replacements:
as_name = alias.asname or alias.name
StrategyUpdater.module_replacements[alias.name]["aliases"].add(as_name)
return node
def visit_ImportFrom(self, node):
# if hasattr(node, "module"):
# if node.module == "freqtrade.strategy.hyper":
# node.module = "freqtrade.strategy"
if node.module in StrategyUpdater.module_replacements:
mod = node.module
StrategyUpdater.module_replacements[node.module]["aliases"].add(mod)
return node
def visit_If(self, node: ast_comments.If):
@@ -182,6 +201,12 @@ class NameUpdater(ast_comments.NodeTransformer):
and node.attr == "nr_of_successful_buys"
):
node.attr = "nr_of_successful_entries"
if isinstance(node.value, ast_comments.Name):
for mod, info in StrategyUpdater.module_replacements.items():
if node.value.id in info["aliases"]:
for old_attr, new_attr in info["replacements"]:
if node.attr == old_attr:
node.attr = new_attr
return node
def visit_ClassDef(self, node):

View File

@@ -9,4 +9,4 @@ def get_dry_run_wallet(config: Config) -> int | float:
if isinstance(_start_cap := config["dry_run_wallet"], float | int):
return _start_cap
else:
return _start_cap.get("stake_currency")
return _start_cap.get(config["stake_currency"], 0.0)

View File

@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2025.7-dev"
__version__ = "2025.8-dev"
if "dev" in __version__:
from pathlib import Path

View File

@@ -6,11 +6,11 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.12.3
mypy==1.16.1
ruff==0.12.7
mypy==1.17.1
pre-commit==4.2.0
pytest==8.4.1
pytest-asyncio==1.0.0
pytest-asyncio==1.1.0
pytest-cov==6.2.1
pytest-mock==3.14.1
pytest-random-order==1.2.0
@@ -24,8 +24,8 @@ time-machine==2.16.0
nbconvert==7.16.6
# mypy types
scipy-stubs==1.16.0.2 # keep in sync with `scipy` in `requirements-hyperopt.txt`
types-cachetools==6.0.0.20250525
scipy-stubs==1.16.1.0 # keep in sync with `scipy` in `requirements-hyperopt.txt`
types-cachetools==6.1.0.20250717
types-filelock==3.2.7
types-requests==2.32.4.20250611
types-tabulate==0.9.0.20241207

View File

@@ -5,7 +5,7 @@
torch==2.7.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==0.29.1
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
stable_baselines3==2.6.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
stable_baselines3==2.7.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
sb3_contrib>=2.2.1
# Progress bar for stable-baselines3 and sb3-contrib
tqdm==4.67.1

View File

@@ -3,10 +3,10 @@
-r requirements-plot.txt
# Required for freqai
scikit-learn==1.7.0
scikit-learn==1.7.1
joblib==1.5.1
catboost==1.2.8; 'arm' not in platform_machine
lightgbm==4.6.0
xgboost==3.0.2
tensorboard==2.19.0
xgboost==3.0.3
tensorboard==2.20.0
datasieve==0.1.9

View File

@@ -2,8 +2,8 @@
-r requirements.txt
# Required for hyperopt
scipy==1.16.0
scikit-learn==1.7.0
scipy==1.16.1
scikit-learn==1.7.1
filelock==3.18.0
optuna==4.4.0
cmaes==0.11.1
cmaes==0.12.0

View File

@@ -1,36 +1,36 @@
numpy==2.3.1
numpy==2.3.2
pandas==2.3.1
bottleneck==1.5.0
numexpr==2.11.0
# Indicator libraries
ft-pandas-ta==0.3.15
ta-lib==0.5.5
technical==1.5.1
technical==1.5.2
ccxt==4.4.94
ccxt==4.4.99
cryptography==45.0.5
aiohttp==3.12.14
SQLAlchemy==2.0.41
python-telegram-bot==22.2
aiohttp==3.12.15
SQLAlchemy==2.0.42
python-telegram-bot==22.3
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.12.3
cachetools==6.1.0
requests==2.32.4
urllib3==2.5.0
certifi==2025.7.14
jsonschema==4.24.0
certifi==2025.8.3
jsonschema==4.25.0
tabulate==0.9.0
pycoingecko==3.2.0
jinja2==3.1.6
joblib==1.5.1
rich==14.0.0
pyarrow==20.0.0; platform_machine != 'armv7l'
rich==14.1.0
pyarrow==21.0.0; platform_machine != 'armv7l'
# Load ticker files 30% faster
python-rapidjson==1.21
# Properly format api responses
orjson==3.10.18
orjson==3.11.1
# Notify systemd
sdnotify==0.3.2

View File

@@ -133,6 +133,8 @@ def test_list_exchanges(capsys):
captured = capsys.readouterr()
assert re.search(r"^binance$", captured.out, re.MULTILINE)
assert re.search(r"^bybit$", captured.out, re.MULTILINE)
# An exchange not supporting futures
assert re.search(r"^kraken$", captured.out, re.MULTILINE)
# Test with --all
args = [
@@ -160,6 +162,32 @@ def test_list_exchanges(capsys):
assert re.search(r"^bingx$", captured.out, re.MULTILINE)
assert re.search(r"^bitmex$", captured.out, re.MULTILINE)
# Only dex
args = [
"list-exchanges",
"--dex",
]
start_list_exchanges(get_args(args))
captured = capsys.readouterr()
assert re.search(r"Exchanges available for Freqtrade.*", captured.out)
assert not re.search(r".*binance.*", captured.out)
assert not re.search(r".*bingx.*", captured.out)
assert re.search(r".*hyperliquid.*", captured.out)
# Only futures
args = [
"list-exchanges",
"--trading-mode",
"futures",
]
start_list_exchanges(get_args(args))
captured = capsys.readouterr()
assert re.search(r"Exchanges available for Freqtrade.*", captured.out)
assert re.search(r".*binance.*", captured.out)
assert not re.search(r".*kraken.*", captured.out)
def test_list_timeframes(mocker, capsys):
api_mock = MagicMock()

View File

@@ -90,3 +90,94 @@ def test_get_tick_size_over_time_real_data(testdatadir):
assert all(result <= 0.0001)
assert all(result >= 0.00000001)
def test_get_tick_size_over_time_small_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": [
0.000000123456,
0.0000001234,
0.000000123,
0.00000012,
0.000000123456,
0.0000001234,
0.00000023456,
0.000000234,
0.000000234,
],
"high": [
0.000000123457,
0.0000001235,
0.000000124,
0.00000013,
0.000000123456,
0.0000001235,
0.00000023457,
0.000000234,
0.000000234,
],
"low": [
0.000000123455,
0.0000001233,
0.000000122,
0.00000011,
0.000000123456,
0.0000001233,
0.00000023455,
0.000000234,
0.000000234,
],
"close": [
0.000000123456,
0.0000001234,
0.000000123,
0.00000012,
0.000000123456,
0.0000001234,
0.00000023456,
0.000000234,
0.000000234,
],
"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.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
assert result.asof("2020-04-01 00:00:00+00:00") == 0.000000001
# Value far past the last date should be the last value
assert result.asof("2025-04-01 00:00:00+00:00") == 0.000000001
assert result.iloc[0] == 0.000000000001

View File

@@ -2506,6 +2506,8 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach
time_machine.move_to(start + timedelta(hours=99, minutes=30))
exchange = get_patched_exchange(mocker, default_conf)
exchange._set_startup_candle_count(default_conf)
mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100)
assert exchange._startup_candle_count == 0
@@ -4954,7 +4956,7 @@ def test_validate_trading_mode_and_margin_mode(
("binance", "margin", {"options": {"defaultType": "margin"}}),
("binance", "futures", {"options": {"defaultType": "swap"}}),
("bybit", "spot", {"options": {"defaultType": "spot"}}),
("bybit", "futures", {"options": {"defaultType": "swap"}}),
("bybit", "futures", {"options": {"defaultType": "swap", "defaultSettle": "USDT"}}),
("gate", "futures", {"options": {"defaultType": "swap"}}),
("hitbtc", "futures", {"options": {"defaultType": "swap"}}),
("kraken", "futures", {"options": {"defaultType": "swap"}}),
@@ -4962,10 +4964,10 @@ def test_validate_trading_mode_and_margin_mode(
("okx", "futures", {"options": {"defaultType": "swap"}}),
],
)
def test__ccxt_config(default_conf, mocker, exchange_name, trading_mode, ccxt_config):
default_conf["trading_mode"] = trading_mode
default_conf["margin_mode"] = "isolated"
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
def test__ccxt_config(default_conf_usdt, mocker, exchange_name, trading_mode, ccxt_config):
default_conf_usdt["trading_mode"] = trading_mode
default_conf_usdt["margin_mode"] = "isolated"
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name)
assert exchange._ccxt_config == ccxt_config

View File

@@ -408,13 +408,21 @@ EXCHANGES = {
"candle_count": 200,
"orderbook_max_entries": 50,
},
"htx": {
"pair": "ETH/BTC",
"stake_currency": "BTC",
"bitget": {
"pair": "BTC/USDT",
"stake_currency": "USDT",
"hasQuoteVolume": True,
"timeframe": "1h",
"candle_count": 1000,
},
# TODO: re-enable htx once certificates work again
# "htx": {
# "pair": "ETH/BTC",
# "stake_currency": "BTC",
# "hasQuoteVolume": True,
# "timeframe": "1h",
# "candle_count": 1000,
# },
"bitvavo": {
"pair": "BTC/EUR",
"stake_currency": "EUR",
@@ -490,7 +498,7 @@ EXCHANGES = {
"pair": "UBTC/USDC",
"stake_currency": "USDC",
"hasQuoteVolume": False,
"timeframe": "1h",
"timeframe": "30m",
"futures": True,
"candle_count": 5000,
"orderbook_max_entries": 20,
@@ -498,6 +506,8 @@ EXCHANGES = {
"hasQuoteVolumeFutures": True,
"leverage_tiers_public": False,
"leverage_in_spot_market": False,
# TODO: re-enable hyperliquid websocket tests
"skip_ws_tests": True,
},
}
@@ -568,12 +578,16 @@ def get_futures_exchange(exchange_name, exchange_conf, class_mocker):
@pytest.fixture(params=EXCHANGES, scope="class")
def exchange(request, exchange_conf, class_mocker):
class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init")
return get_exchange(request.param, exchange_conf)
exchange, name = get_exchange(request.param, exchange_conf)
yield exchange, name
exchange.close()
@pytest.fixture(params=EXCHANGES, scope="class")
def exchange_futures(request, exchange_conf, class_mocker):
return get_futures_exchange(request.param, exchange_conf, class_mocker)
exchange, name = get_futures_exchange(request.param, exchange_conf, class_mocker)
yield exchange, name
exchange.close()
@pytest.fixture(params=["spot", "futures"], scope="class")

View File

@@ -270,9 +270,7 @@ class TestCCXTExchange:
assert exch.klines(pair_tf).iloc[-1]["date"] >= timeframe_to_prev_date(timeframe, now)
assert exch.klines(pair_tf)["date"].astype(int).iloc[0] // 1e6 == since_ms
def ccxt__async_get_candle_history(
self, exchange, exchangename, pair, timeframe, candle_type, factor=0.9
):
def _ccxt__async_get_candle_history(self, exchange, pair, timeframe, candle_type, factor=0.9):
timeframe_ms = timeframe_to_msecs(timeframe)
now = timeframe_to_prev_date(timeframe, datetime.now(UTC))
for offset in (360, 120, 30, 10, 5, 2):
@@ -304,7 +302,7 @@ class TestCCXTExchange:
pytest.skip("Exchange does not support candle history")
pair = EXCHANGES[exchangename]["pair"]
timeframe = EXCHANGES[exchangename]["timeframe"]
self.ccxt__async_get_candle_history(exc, exchangename, pair, timeframe, CandleType.SPOT)
self._ccxt__async_get_candle_history(exc, pair, timeframe, CandleType.SPOT)
@pytest.mark.parametrize(
"candle_type",
@@ -315,7 +313,7 @@ class TestCCXTExchange:
],
)
def test_ccxt__async_get_candle_history_futures(
self, exchange_futures: EXCHANGE_FIXTURE_TYPE, candle_type
self, exchange_futures: EXCHANGE_FIXTURE_TYPE, candle_type: CandleType
):
exchange, exchangename = exchange_futures
pair = EXCHANGES[exchangename].get("futures_pair", EXCHANGES[exchangename]["pair"])
@@ -324,9 +322,8 @@ class TestCCXTExchange:
timeframe = exchange._ft_has.get(
"funding_fee_timeframe", exchange._ft_has["mark_ohlcv_timeframe"]
)
self.ccxt__async_get_candle_history(
self._ccxt__async_get_candle_history(
exchange,
exchangename,
pair=pair,
timeframe=timeframe,
candle_type=candle_type,
@@ -383,6 +380,10 @@ class TestCCXTExchange:
this_hour = timeframe_to_prev_date(expected_tf)
prev_hour = timeframe_to_prev_date(expected_tf, this_hour - timedelta(minutes=1))
# Mark price must be available for the currently open candle (as well as older candles,
# even though the test only asserts the last two).
# This is a requirement to have funding fee calculations available correctly and timely
# right as the funding fee applies (e.g. at 08:00).
assert mark_candles[mark_candles["date"] == prev_hour].iloc[0]["open"] != 0.0
assert mark_candles[mark_candles["date"] == this_hour].iloc[0]["open"] != 0.0
@@ -396,14 +397,15 @@ class TestCCXTExchange:
)
assert isinstance(funding_fee, float)
# assert funding_fee > 0
assert funding_fee != 0
def test_ccxt__async_get_trade_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
def test_ccxt__async_get_trade_history(self, exchange: EXCHANGE_FIXTURE_TYPE, mocker):
exch, exchangename = exchange
if not (lookback := EXCHANGES[exchangename].get("trades_lookback_hours")):
pytest.skip("test_fetch_trades not enabled for this exchange")
pair = EXCHANGES[exchangename]["pair"]
since = int((datetime.now(UTC) - timedelta(hours=lookback)).timestamp() * 1000)
nvspy = mocker.spy(exch, "_get_trade_pagination_next_value")
res = exch.loop.run_until_complete(exch._async_get_trade_history(pair, since, None, None))
assert len(res) == 2
res_pair, res_trades = res
@@ -411,6 +413,14 @@ class TestCCXTExchange:
assert isinstance(res_trades, list)
assert res_trades[0][0] >= since
assert len(res_trades) > 1200
assert nvspy.call_count > 5
if exchangename == "kraken":
# for Kraken, the pagination value is added to the last trade result by ccxt.
# We therefore expect that the last row has one additional field
# Pick a random spy call
trades_orig = nvspy.call_args_list[2][0][0]
assert len(trades_orig[-1].get("info")) > len(trades_orig[-2].get("info"))
def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
@@ -520,3 +530,35 @@ class TestCCXTExchange:
exch, exchangename = exchange
for method in EXCHANGES[exchangename].get("private_methods", []):
assert hasattr(exch._api, method)
def test_ccxt_bitget_ohlcv_candle_limit(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
if exchangename != "bitget":
pytest.skip("This test is only for the Bitget exchange")
timeframes = ("1m", "5m", "1h")
for timeframe in timeframes:
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK) == 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE) == 200
start_time = dt_ts(dt_now() - timedelta(days=17))
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
start_time = dt_ts(dt_now() - timedelta(days=48))
length = 200 if timeframe in ("1m", "5m") else 1000
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
start_time = dt_ts(dt_now() - timedelta(days=61))
length = 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == length
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 200
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200

View File

@@ -35,21 +35,22 @@ class TestCCXTExchangeWs:
break
sleep(1)
caplog.set_level(logging.DEBUG)
res = exch.refresh_latest_ohlcv([pair_tf])
assert m_cand.call_count == 1
# Currently open candle
next_candle = timeframe_to_prev_date(timeframe, dt_now())
now = next_candle - timedelta(seconds=1)
# Currently closed candle
curr_candle = timeframe_to_prev_date(timeframe, now)
curr_candle = timeframe_to_prev_date(timeframe, next_candle - timedelta(seconds=1))
assert pair_tf in exch._exchange_ws._klines_watching
assert pair_tf in exch._exchange_ws._klines_scheduled
assert res[pair_tf] is not None
df1 = res[pair_tf]
caplog.set_level(logging.DEBUG)
assert df1.iloc[-1]["date"] == curr_candle
assert df1.iloc[-1]["date"] == curr_candle, (
f"Expected {curr_candle}, got {df1.iloc[-1]['date']} for {pair_tf}, now: {dt_now()}"
)
# Wait until the next candle (might be up to 1 minute).
while True:

View File

@@ -1343,6 +1343,44 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
}
def test_api_profit_all(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient
ftbot.config["tradable_balance_ratio"] = 1
ftbot.config["trading_mode"] = TradingMode.FUTURES
patch_get_signal(ftbot)
mocker.patch.multiple(
EXMS,
get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker,
get_fee=fee,
markets=PropertyMock(return_value=markets),
)
rc = client_get(client, f"{BASE_URI}/profit_all")
assert_response(rc, 200)
response = rc.json()
assert "all" in response
assert "long" in response
assert "short" in response
assert response["all"]["trade_count"] == 0
create_mock_trades_usdt(fee, is_short=None)
rc = client_get(client, f"{BASE_URI}/profit_all")
assert_response(rc, 200)
response = rc.json()
assert response["all"]["trade_count"] == 7
assert response["long"]["trade_count"] == 2
assert response["short"]["trade_count"] == 5
assert pytest.approx(response["all"]["profit_all_coin"]) == 22.58997755
assert pytest.approx(response["long"]["profit_all_coin"]) == -20.0498903
assert pytest.approx(response["short"]["profit_all_coin"]) == 42.639867
assert response["all"]["best_pair"] == "NEO/USDT"
assert response["long"]["best_pair"] == ""
assert response["short"]["best_pair"] == "NEO/USDT"
@pytest.mark.parametrize("is_short", [True, False])
def test_api_stats(botclient, mocker, ticker, fee, markets, is_short):
ftbot, client = botclient

View File

@@ -42,8 +42,10 @@ def test_strategy_updater_methods(default_conf, caplog) -> None:
instance_strategy_updater = StrategyUpdater()
modified_code1 = instance_strategy_updater.update_code(
"""
import numpy as np
class testClass(IStrategy):
def populate_buy_trend():
some_variable = np.NaN
pass
def populate_sell_trend():
pass
@@ -62,6 +64,7 @@ class testClass(IStrategy):
assert "check_exit_timeout" in modified_code1
assert "custom_exit" in modified_code1
assert "INTERFACE_VERSION = 3" in modified_code1
assert "np.nan" in modified_code1
def test_strategy_updater_params(default_conf, caplog) -> None:

View File

@@ -0,0 +1,19 @@
import pytest
from freqtrade.util import get_dry_run_wallet
@pytest.mark.parametrize(
"wallet,stake_currency,expected",
[
(1000, "USDT", 1000),
({"USDT": 1000, "USDC": 500}, "USDT", 1000),
({"USDT": 1000, "USDC": 500}, "USDC", 500),
({"USDT": 1000, "USDC": 500}, "NOCURR", 0.0),
],
)
def test_get_dry_run_wallet(default_conf_usdt, wallet, stake_currency, expected):
# As int
default_conf_usdt["dry_run_wallet"] = wallet
default_conf_usdt["stake_currency"] = stake_currency
assert get_dry_run_wallet(default_conf_usdt) == expected