Merge branch 'freqtrade:develop' into main-stash

This commit is contained in:
Stefano
2025-12-03 09:10:15 +09:00
committed by GitHub
32 changed files with 289 additions and 108 deletions

View File

@@ -2,7 +2,7 @@ version: 2
updates:
- package-ecosystem: docker
cooldown:
default-days: 4
default-days: 7
directories:
- "/"
- "/docker"
@@ -16,7 +16,7 @@ updates:
- package-ecosystem: devcontainers
directory: "/"
cooldown:
default-days: 4
default-days: 7
schedule:
interval: daily
open-pull-requests-limit: 10
@@ -24,7 +24,7 @@ updates:
- package-ecosystem: pip
directory: "/"
cooldown:
default-days: 4
default-days: 7
exclude:
- ccxt
schedule:
@@ -51,7 +51,7 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
cooldown:
default-days: 4
default-days: 7
schedule:
interval: "weekly"
open-pull-requests-limit: 10

View File

@@ -15,7 +15,7 @@ jobs:
environment:
name: develop
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
@@ -34,7 +34,7 @@ jobs:
run: python build_helpers/binance_update_lev_tiers.py
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: freqtrade/exchange/binance_leverage_tiers.json

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
@@ -38,7 +38,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
activate-environment: true
enable-cache: true
@@ -177,7 +177,7 @@ jobs:
name: "Mypy Version Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
@@ -195,7 +195,7 @@ jobs:
name: "Pre-commit checks"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
@@ -208,7 +208,7 @@ jobs:
name: "Documentation build"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
@@ -240,7 +240,7 @@ jobs:
name: "Tests and Linting - Online tests"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
@@ -250,7 +250,7 @@ jobs:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
activate-environment: true
enable-cache: true
@@ -320,7 +320,7 @@ jobs:
with:
jobs: ${{ toJSON(needs) }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
@@ -367,7 +367,7 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
@@ -396,7 +396,7 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false

View File

@@ -19,7 +19,7 @@ jobs:
name: Deploy Docs through mike
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: true

View File

@@ -24,7 +24,7 @@ jobs:
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
- name: Login to GitHub Container Registry

View File

@@ -33,7 +33,7 @@ jobs:
if: github.repository == 'freqtrade/freqtrade'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
@@ -152,7 +152,7 @@ jobs:
if: github.repository == 'freqtrade/freqtrade'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false

View File

@@ -11,7 +11,7 @@ jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false

View File

@@ -13,7 +13,7 @@ jobs:
auto-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.0
with:
persist-credentials: false
@@ -28,7 +28,7 @@ jobs:
- name: Run auto-update
run: pre-commit autoupdate
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: .pre-commit-config.yaml

View File

@@ -22,9 +22,9 @@ jobs:
# actions: read # only needed for private repos
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@v6.0.0
with:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0

View File

@@ -21,7 +21,7 @@ repos:
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.18.2"
rev: "v1.19.0"
hooks:
- id: mypy
exclude: build_helpers
@@ -31,7 +31,7 @@ repos:
- types-requests==2.32.4.20250913
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20251115
- scipy-stubs==1.16.3.0
- scipy-stubs==1.16.3.1
- SQLAlchemy==2.0.44
# stages: [push]
@@ -44,7 +44,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.14.6'
rev: 'v0.14.7'
hooks:
- id: ruff
- id: ruff-format
@@ -83,6 +83,6 @@ repos:
# Ensure github actions remain safe
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.16.3
rev: v1.18.0
hooks:
- id: zizmor

View File

@@ -2,6 +2,6 @@ markdown==3.10
mkdocs==1.6.1
mkdocs-material==9.7.0
mdx_truly_sane_lists==1.3
pymdown-extensions==10.17.1
pymdown-extensions==10.17.2
jinja2==3.1.6
mike==2.1.3

View File

@@ -634,7 +634,7 @@ class AwesomeStrategy(IStrategy):
## Custom order price rules
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
By default, freqtrade use the orderbook to automatically set an order price ([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.

View File

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

View File

@@ -308,11 +308,15 @@ def _download_pair_history(
candle_type=candle_type,
until_ms=until_ms if until_ms else None,
)
logger.info(f"Downloaded data for {pair} with length {len(new_dataframe)}.")
logger.info(
f"Downloaded data for {pair}, {timeframe}, {candle_type} with length "
f"{len(new_dataframe)}."
)
else:
new_dataframe = pair_candles
logger.info(
f"Downloaded data for {pair} with length {len(new_dataframe)}. Parallel Method."
f"Downloaded data for {pair}, {timeframe}, {candle_type} with length "
f"{len(new_dataframe)}. Parallel Method."
)
if data.empty:

View File

@@ -104,6 +104,7 @@ from freqtrade.misc import (
deep_merge_dicts,
file_dump_json,
file_load_json,
safe_value_fallback,
safe_value_fallback2,
)
from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now
@@ -1119,6 +1120,7 @@ class Exchange:
leverage: float,
params: dict | None = None,
stop_loss: bool = False,
stop_price: float | None = None,
) -> CcxtOrder:
now = dt_now()
order_id = f"dry_run_{side}_{pair}_{now.timestamp()}"
@@ -1145,7 +1147,7 @@ class Exchange:
}
if stop_loss:
dry_order["info"] = {"stopPrice": dry_order["price"]}
dry_order[self._ft_has["stop_price_prop"]] = dry_order["price"]
dry_order[self._ft_has["stop_price_prop"]] = stop_price or dry_order["price"]
# Workaround to avoid filling stoploss orders immediately
dry_order["ft_order_type"] = "stoploss"
orderbook: OrderBook | None = None
@@ -1163,7 +1165,11 @@ class Exchange:
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
# Update market order pricing
average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
slippage = 0.05
worst_rate = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
average = self.get_dry_market_fill_price(
pair, side, amount, rate, worst_rate, orderbook
)
dry_order.update(
{
"average": average,
@@ -1203,7 +1209,13 @@ class Exchange:
return dry_order
def get_dry_market_fill_price(
self, pair: str, side: str, amount: float, rate: float, orderbook: OrderBook | None
self,
pair: str,
side: str,
amount: float,
rate: float,
worst_rate: float,
orderbook: OrderBook | None,
) -> float:
"""
Get the market order fill price based on orderbook interpolation
@@ -1212,8 +1224,6 @@ class Exchange:
if not orderbook:
orderbook = self.fetch_l2_order_book(pair, 20)
ob_type: OBLiteral = "asks" if side == "buy" else "bids"
slippage = 0.05
max_slippage_val = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
remaining_amount = amount
filled_value = 0.0
@@ -1237,11 +1247,10 @@ class Exchange:
forecast_avg_filled_price = max(filled_value, 0) / amount
# Limit max. slippage to specified value
if side == "buy":
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
forecast_avg_filled_price = min(forecast_avg_filled_price, worst_rate)
else:
forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
forecast_avg_filled_price = max(forecast_avg_filled_price, worst_rate)
return self.price_to_precision(pair, forecast_avg_filled_price)
return rate
@@ -1253,13 +1262,15 @@ class Exchange:
limit: float,
orderbook: OrderBook | None = None,
offset: float = 0.0,
is_stop: bool = False,
) -> bool:
if not self.exchange_has("fetchL2OrderBook"):
return True
# True unless checking a stoploss order
return not is_stop
if not orderbook:
orderbook = self.fetch_l2_order_book(pair, 1)
try:
if side == "buy":
if (side == "buy" and not is_stop) or (side == "sell" and is_stop):
price = orderbook["asks"][0][0]
if limit * (1 - offset) >= price:
return True
@@ -1278,6 +1289,38 @@ class Exchange:
"""
Check dry-run limit order fill and update fee (if it filled).
"""
if order["status"] != "closed" and order.get("ft_order_type") == "stoploss":
pair = order["symbol"]
if not orderbook and self.exchange_has("fetchL2OrderBook"):
orderbook = self.fetch_l2_order_book(pair, 20)
price = safe_value_fallback(order, self._ft_has["stop_price_prop"], "price")
crossed = self._dry_is_price_crossed(
pair, order["side"], price, orderbook, is_stop=True
)
if crossed:
average = self.get_dry_market_fill_price(
pair,
order["side"],
order["amount"],
price,
worst_rate=order["price"],
orderbook=orderbook,
)
order.update(
{
"status": "closed",
"filled": order["amount"],
"remaining": 0,
"average": average,
"cost": order["amount"] * average,
}
)
self.add_dry_order_fee(
pair,
order,
"taker" if immediate else "maker",
)
return order
if (
order["status"] != "closed"
and order["type"] in ["limit"]
@@ -1517,8 +1560,9 @@ class Exchange:
ordertype,
side,
amount,
stop_price_norm,
limit_rate or stop_price_norm,
stop_loss=True,
stop_price=stop_price_norm,
leverage=leverage,
)
return dry_order
@@ -3740,10 +3784,11 @@ class Exchange:
:param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
:param futures_funding_rate: Fake funding rate to use if funding_rates are not available
"""
relevant_cols = ["date", "open_mark", "open_fund"]
if futures_funding_rate is None:
return mark_rates.merge(
funding_rates, on="date", how="inner", suffixes=["_mark", "_fund"]
)
)[relevant_cols]
else:
if len(funding_rates) == 0:
# No funding rate candles - full fillup with fallback variable
@@ -3756,7 +3801,7 @@ class Exchange:
"low": "low_mark",
"volume": "volume_mark",
}
)
)[relevant_cols]
else:
# Fill up missing funding_rate candles with fallback value
@@ -3764,7 +3809,7 @@ class Exchange:
funding_rates, on="date", how="left", suffixes=["_mark", "_fund"]
)
combined["open_fund"] = combined["open_fund"].fillna(futures_funding_rate)
return combined
return combined[relevant_cols]
def calculate_funding_fees(
self,

View File

@@ -1063,7 +1063,16 @@ class FreqtradeBot(LoggingMixin):
return True
def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
def cancel_stoploss_on_exchange(self, trade: Trade, allow_nonblocking: bool = False) -> Trade:
"""
Cancels on exchange stoploss orders for the given trade.
:param trade: Trade for which to cancel stoploss order
:param allow_nonblocking: If True, will skip cancelling stoploss on exchange
if the exchange supports blocking stoploss orders.
"""
if allow_nonblocking and not self.exchange.get_option("stoploss_blocks_assets", True):
logger.info(f"Skipping cancelling stoploss on exchange for {trade}.")
return trade
# First cancelling stoploss on exchange ...
for oslo in trade.open_sl_orders:
try:
@@ -2088,7 +2097,7 @@ class FreqtradeBot(LoggingMixin):
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
# First cancelling stoploss on exchange ...
trade = self.cancel_stoploss_on_exchange(trade)
trade = self.cancel_stoploss_on_exchange(trade, allow_nonblocking=True)
order_type = ordertype or self.strategy.order_types[exit_type]
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
@@ -2378,6 +2387,8 @@ class FreqtradeBot(LoggingMixin):
self.strategy.ft_stoploss_adjust(
current_rate, trade, datetime.now(UTC), profit, 0, after_fill=True
)
if not trade.is_open:
self.cancel_stoploss_on_exchange(trade)
# Updating wallets when order is closed
self.wallets.update()
return trade

View File

@@ -48,7 +48,7 @@ from freqtrade.leverage import interest
from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.base import ModelBase, SessionType
from freqtrade.persistence.custom_data import CustomDataWrapper, _CustomData
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none, round_value
logger = logging.getLogger(__name__)
@@ -654,9 +654,10 @@ class LocalTrade:
)
return (
f"Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, "
f"is_short={self.is_short or False}, leverage={self.leverage or 1.0}, "
f"open_rate={self.open_rate:.8f}, open_since={open_since})"
f"Trade(id={self.id}, pair={self.pair}, amount={round_value(self.amount, 8)}, "
f"is_short={self.is_short or False}, "
f"leverage={round_value(self.leverage or 1.0, 1)}, "
f"open_rate={round_value(self.open_rate, 8)}, open_since={open_since})"
)
def to_json(self, minified: bool = False) -> dict[str, Any]:

View File

@@ -90,15 +90,16 @@ def dt_humanize_delta(dt: datetime):
return humanize.naturaltime(dt)
def format_date(date: datetime | None) -> str:
def format_date(date: datetime | None, fallback: str = "") -> str:
"""
Return a formatted date string.
Returns an empty string if date is None.
:param date: datetime to format
:param fallback: value to return if date is None
"""
if date:
return date.strftime(DATETIME_PRINT_FORMAT)
return ""
return fallback
def format_ms_time(date: int | float) -> str:

View File

@@ -23,7 +23,7 @@ def strip_trailing_zeros(value: str) -> str:
return value.rstrip("0").rstrip(".")
def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str:
def round_value(value: float | None, decimals: int, keep_trailing_zeros=False) -> str:
"""
Round value to given decimals
:param value: Value to be rounded
@@ -31,7 +31,7 @@ def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str:
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
:return: Rounded value as string
"""
if isnan(value):
if value is None or isnan(value):
return "N/A"
val = f"{value:.{decimals}f}"
if not keep_trailing_zeros:

View File

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

View File

@@ -6,9 +6,9 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.14.5
ruff==0.14.6
mypy==1.18.2
pre-commit==4.4.0
pre-commit==4.5.0
pytest==9.0.1
pytest-asyncio==1.3.0
pytest-cov==7.0.0
@@ -18,13 +18,13 @@ pytest-timeout==2.4.0
pytest-xdist==3.8.0
isort==7.0.0
# For datetime mocking
time-machine==3.0.0
time-machine==3.1.0
# Convert jupyter notebooks to markdown documents
nbconvert==7.16.6
# mypy types
scipy-stubs==1.16.3.0 # keep in sync with `scipy` in `requirements-hyperopt.txt`
scipy-stubs==1.16.3.1 # keep in sync with `scipy` in `requirements-hyperopt.txt`
types-cachetools==6.2.0.20251022
types-filelock==3.2.7
types-requests==2.32.4.20250913

View File

@@ -7,6 +7,6 @@ scikit-learn==1.7.2
joblib==1.5.2
catboost==1.2.8; 'arm' not in platform_machine
lightgbm==4.6.0
xgboost==3.1.1
xgboost==3.1.2
tensorboard==2.20.0
datasieve==0.1.9

View File

@@ -7,7 +7,7 @@ ft-pandas-ta==0.3.16
ta-lib==0.6.8
technical==1.5.3
ccxt==4.5.20
ccxt==4.5.22
cryptography==46.0.3
aiohttp==3.13.2
SQLAlchemy==2.0.44
@@ -37,8 +37,8 @@ orjson==3.11.4
sdnotify==0.3.2
# API Server
fastapi==0.121.3
pydantic==2.12.4
fastapi==0.122.0
pydantic==2.12.5
uvicorn==0.38.0
pyjwt==2.10.1
aiofiles==25.1.0

View File

@@ -896,7 +896,7 @@ def test_download_pair_history_with_pair_candles(mocker, default_conf, tmp_path,
assert get_historic_ohlcv_mock.call_count == 0
# Verify the log message indicating parallel method was used (line 315-316)
assert log_has("Downloaded data for TEST/BTC with length 3. Parallel Method.", caplog)
assert log_has("Downloaded data for TEST/BTC, 5m, spot with length 3. Parallel Method.", caplog)
# Verify data was stored
assert data_handler_mock.ohlcv_store.call_count == 1

View File

@@ -157,7 +157,8 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
assert "type" in order
assert order["type"] == order_type
assert order["price"] == 220
assert order["price"] == 217.8
assert order["stopPrice"] == 220
assert order["amount"] == 1

View File

@@ -1111,21 +1111,29 @@ def test_create_dry_run_order_fees(
@pytest.mark.parametrize(
"side,limit,offset,expected",
"side,limit,offset,is_stop,expected",
[
("buy", 46.0, 0.0, True),
("buy", 26.0, 0.0, True),
("buy", 25.55, 0.0, False),
("buy", 1, 0.0, False), # Very far away
("sell", 25.5, 0.0, True),
("sell", 50, 0.0, False), # Very far away
("sell", 25.58, 0.0, False),
("sell", 25.563, 0.01, False),
("sell", 5.563, 0.01, True),
("buy", 46.0, 0.0, False, True),
("buy", 46.0, 0.0, True, False),
("buy", 26.0, 0.0, False, True),
("buy", 26.0, 0.0, True, False), # Stop - didn't trigger
("buy", 25.55, 0.0, False, False),
("buy", 25.55, 0.0, True, True), # Stop - triggered
("buy", 1, 0.0, False, False), # Very far away
("buy", 1, 0.0, True, True), # Current price is above stop - triggered
("sell", 25.5, 0.0, False, True),
("sell", 50, 0.0, False, False), # Very far away
("sell", 25.58, 0.0, False, False),
("sell", 25.563, 0.01, False, False),
("sell", 25.563, 0.0, True, False), # stop order - Not triggered, best bid
("sell", 25.566, 0.0, True, True), # stop order - triggered
("sell", 26, 0.01, True, True), # stop order - triggered
("sell", 5.563, 0.01, False, True),
("sell", 5.563, 0.0, True, False), # stop order - not triggered
],
)
def test__dry_is_price_crossed_with_orderbook(
default_conf, mocker, order_book_l2_usd, side, limit, offset, expected
default_conf, mocker, order_book_l2_usd, side, limit, offset, is_stop, expected
):
# Best bid 25.563
# Best ask 25.566
@@ -1134,14 +1142,14 @@ def test__dry_is_price_crossed_with_orderbook(
exchange.fetch_l2_order_book = order_book_l2_usd
orderbook = order_book_l2_usd.return_value
result = exchange._dry_is_price_crossed(
"LTC/USDT", side, limit, orderbook=orderbook, offset=offset
"LTC/USDT", side, limit, orderbook=orderbook, offset=offset, is_stop=is_stop
)
assert result is expected
assert order_book_l2_usd.call_count == 0
# Test without passing orderbook
order_book_l2_usd.reset_mock()
result = exchange._dry_is_price_crossed("LTC/USDT", side, limit, offset=offset)
result = exchange._dry_is_price_crossed("LTC/USDT", side, limit, offset=offset, is_stop=is_stop)
assert result is expected
@@ -1165,7 +1173,10 @@ def test__dry_is_price_crossed_without_orderbook_support(default_conf, mocker):
exchange.fetch_l2_order_book = MagicMock()
mocker.patch(f"{EXMS}.exchange_has", return_value=False)
assert exchange._dry_is_price_crossed("LTC/USDT", "buy", 1.0)
assert exchange._dry_is_price_crossed("LTC/USDT", "sell", 1.0)
assert exchange.fetch_l2_order_book.call_count == 0
assert not exchange._dry_is_price_crossed("LTC/USDT", "buy", 1.0, is_stop=True)
assert not exchange._dry_is_price_crossed("LTC/USDT", "sell", 1.0, is_stop=True)
@pytest.mark.parametrize(
@@ -1176,7 +1187,7 @@ def test__dry_is_price_crossed_without_orderbook_support(default_conf, mocker):
(False, False, "sell", 1.0, "open", None, 0, None),
],
)
def test_check_dry_limit_order_filled_parametrized(
def test_check_dry_limit_order_filled(
default_conf,
mocker,
crossed,
@@ -1220,6 +1231,70 @@ def test_check_dry_limit_order_filled_parametrized(
assert fee_mock.call_count == expected_calls
@pytest.mark.parametrize(
"immediate,crossed,expected_status,expected_fee_type",
[
(True, True, "closed", "taker"),
(False, True, "closed", "maker"),
(True, False, "open", None),
],
)
def test_check_dry_limit_order_filled_stoploss(
default_conf, mocker, immediate, crossed, expected_status, expected_fee_type, order_book_l2_usd
):
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.multiple(
EXMS,
exchange_has=MagicMock(return_value=True),
_dry_is_price_crossed=MagicMock(return_value=crossed),
fetch_l2_order_book=order_book_l2_usd,
)
average_mock = mocker.patch(f"{EXMS}.get_dry_market_fill_price", return_value=24.25)
fee_mock = mocker.patch(
f"{EXMS}.add_dry_order_fee",
autospec=True,
side_effect=lambda self, pair, dry_order, taker_or_maker: dry_order,
)
amount = 1.75
order = {
"symbol": "LTC/USDT",
"status": "open",
"type": "limit",
"side": "sell",
"amount": amount,
"filled": 0.0,
"remaining": amount,
"price": 25.0,
"average": 0.0,
"cost": 0.0,
"fee": None,
"ft_order_type": "stoploss",
"stopLossPrice": 24.5,
}
result = exchange.check_dry_limit_order_filled(order, immediate=immediate)
assert result["status"] == expected_status
assert order_book_l2_usd.call_count == 1
if crossed:
assert result["filled"] == amount
assert result["remaining"] == 0
assert result["average"] == 24.25
assert result["cost"] == pytest.approx(amount * 24.25)
assert average_mock.call_count == 1
assert fee_mock.call_count == 1
assert fee_mock.call_args[0][1] == "LTC/USDT"
assert fee_mock.call_args[0][3] == expected_fee_type
else:
assert result["filled"] == 0.0
assert result["remaining"] == amount
assert result["average"] == 0.0
assert average_mock.call_count == 0
assert fee_mock.call_count == 0
@pytest.mark.parametrize(
"side,price,filled,converted",
[
@@ -5229,6 +5304,7 @@ def test_combine_funding_and_mark(
{"date": trade_date, "open": mark_price},
]
)
# Test fallback to futures funding rate for missing funding rates
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
if futures_funding_rate is not None:
@@ -5256,6 +5332,33 @@ def test_combine_funding_and_mark(
assert len(df) == 0
# Test fallback to futures funding rate for middle missing funding rate
funding_rates = DataFrame(
[
{"date": prior2_date, "open": funding_rate},
# missing 1 hour
{"date": trade_date, "open": funding_rate},
],
)
mark_rates = DataFrame(
[
{"date": prior2_date, "open": mark_price},
{"date": prior_date, "open": mark_price},
{"date": trade_date, "open": mark_price},
]
)
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
if futures_funding_rate is not None:
assert len(df) == 3
assert df.iloc[0]["open_fund"] == funding_rate
assert df.iloc[1]["open_fund"] == futures_funding_rate
assert df.iloc[2]["open_fund"] == funding_rate
assert df["date"].to_list() == [prior2_date, prior_date, trade_date]
else:
assert len(df) == 2
assert df["date"].to_list() == [prior2_date, trade_date]
@pytest.mark.parametrize(
"exchange,rate_start,rate_end,d1,d2,amount,expected_fees",

View File

@@ -123,7 +123,8 @@ def test_create_stoploss_order_dry_run_htx(default_conf, mocker):
assert "type" in order
assert order["type"] == order_type
assert order["price"] == 220
assert order["price"] == 217.8
assert order["stopPrice"] == 220
assert order["amount"] == 1

View File

@@ -2548,9 +2548,9 @@ def test_manage_open_orders_exception(
caplog.clear()
freqtrade.manage_open_orders()
assert log_has_re(
r"Cannot query order for Trade\(id=1, pair=ADA/USDT, amount=30.00000000, "
r"is_short=False, leverage=1.0, "
r"open_rate=2.00000000, open_since="
r"Cannot query order for Trade\(id=1, pair=ADA/USDT, amount=30, "
r"is_short=False, leverage=1, "
r"open_rate=2, open_since="
f"{open_trade_usdt.open_date.strftime('%Y-%m-%d %H:%M:%S')}"
r"\) due to Traceback \(most recent call last\):\n*",
caplog,
@@ -3751,8 +3751,8 @@ def test_get_real_amount_quote(
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == (amount * 0.001)
assert log_has(
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,"
" leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.008.",
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, is_short=False,"
" leverage=1, open_rate=0.245441, open_since=closed), fee=0.008.",
caplog,
)
@@ -3805,8 +3805,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) is None
assert log_has(
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) failed: "
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed) failed: "
"myTrade-dict empty found",
caplog,
)
@@ -3825,8 +3825,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
0,
True,
(
"Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False, "
"leverage=1.0, open_rate=0.24544100, open_since=closed) [buy]: 0.00094518 BNB -"
"Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8, is_short=False, "
"leverage=1, open_rate=0.245441, open_since=closed) [buy]: 0.00094518 BNB -"
" rate: None"
),
),
@@ -3836,8 +3836,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
0.004,
False,
(
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.004."
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed), fee=0.004."
),
),
# invalid, no currency in from fee dict
@@ -3941,8 +3941,8 @@ def test_get_real_amount_multi(
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == expected_amount
assert log_has(
(
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), "
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed), "
f"fee={expected_amount}."
),
caplog,

View File

@@ -50,7 +50,14 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
stoploss_order_mock = MagicMock(side_effect=stop_orders)
# Sell 3rd trade (not called for the first trade)
should_sell_mock = MagicMock(side_effect=[[], [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]])
cancel_order_mock = MagicMock()
def patch_stoploss(order_id, *args, **kwargs):
slo = stoploss_order_open.copy()
slo["id"] = order_id
slo["status"] = "canceled"
return slo
cancel_order_mock = MagicMock(side_effect=patch_stoploss)
mocker.patch.multiple(
EXMS,
create_stoploss=stoploss,
@@ -793,9 +800,13 @@ def test_dca_handle_similar_open_order(
# Should Create a new exit order
freqtrade.exchange.amount_to_contract_precision = MagicMock(return_value=2)
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-2)
msg = r"Skipping cancelling stoploss on exchange for.*"
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
assert not log_has_re(msg, caplog)
freqtrade.process()
assert log_has_re(msg, caplog)
trade = Trade.get_trades().first()
assert trade.orders[-2].status == "closed"

View File

@@ -372,8 +372,8 @@ def test_borrowed(fee, is_short, lev, borrowed, trading_mode):
@pytest.mark.parametrize(
"is_short,open_rate,close_rate,lev,profit,trading_mode",
[
(False, 2.0, 2.2, 1.0, 0.09451372, spot),
(True, 2.2, 2.0, 3.0, 0.25894253, margin),
(False, 2, 2.2, 1, 0.09451372, spot),
(True, 2.2, 2.0, 3, 0.25894253, margin),
],
)
@pytest.mark.usefixtures("init_persistence")
@@ -493,8 +493,8 @@ def test_update_limit_order(
assert trade.close_date is None
assert log_has_re(
f"LIMIT_{entry_side.upper()} has been fulfilled for "
r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, "
r"Trade\(id=2, pair=ADA/USDT, amount=30, "
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}, "
r"open_since=.*\).",
caplog,
)
@@ -511,8 +511,8 @@ def test_update_limit_order(
assert trade.close_date is not None
assert log_has_re(
f"LIMIT_{exit_side.upper()} has been fulfilled for "
r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, "
r"Trade\(id=2, pair=ADA/USDT, amount=30, "
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}, "
r"open_since=.*\).",
caplog,
)
@@ -545,8 +545,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
assert trade.close_date is None
assert log_has_re(
r"MARKET_BUY has been fulfilled for Trade\(id=1, "
r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, "
r"open_rate=2.00000000, open_since=.*\).",
r"pair=ADA/USDT, amount=30, is_short=False, leverage=1, "
r"open_rate=2, open_since=.*\).",
caplog,
)
@@ -561,8 +561,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
assert trade.close_date is not None
assert log_has_re(
r"MARKET_SELL has been fulfilled for Trade\(id=1, "
r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, "
r"open_rate=2.00000000, open_since=.*\).",
r"pair=ADA/USDT, amount=30, is_short=False, leverage=1, "
r"open_rate=2, open_since=.*\).",
caplog,
)

View File

@@ -108,6 +108,7 @@ def test_format_date() -> None:
date = datetime(2021, 9, 30, 22, 59, 3, 455555, tzinfo=UTC)
assert format_date(date) == "2021-09-30 22:59:03"
assert format_date(None) == ""
assert format_date(None, "closed") == "closed"
def test_format_ms_time_detailed() -> None:

View File

@@ -57,6 +57,8 @@ def test_round_value():
assert round_value(222.2, 0, True) == "222"
assert round_value(float("nan"), 0, True) == "N/A"
assert round_value(float("nan"), 10, True) == "N/A"
assert round_value(None, 10, True) == "N/A"
assert round_value(None, 1, True) == "N/A"
def test_format_duration():