mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-12-02 18:13:04 +00:00
Compare commits
50 Commits
stable
...
fix/sloe_h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faf552837a | ||
|
|
4b2d099258 | ||
|
|
4e6ea1d2ba | ||
|
|
197623839d | ||
|
|
6d8fa76708 | ||
|
|
d7eb49d5d0 | ||
|
|
659509e085 | ||
|
|
2b729c2527 | ||
|
|
38151423b8 | ||
|
|
2f990b773d | ||
|
|
20d253dd41 | ||
|
|
6e2165b5e5 | ||
|
|
6860904bb9 | ||
|
|
450dc23374 | ||
|
|
d781f09b46 | ||
|
|
dbde8332f7 | ||
|
|
f5f1785b17 | ||
|
|
7d01928df0 | ||
|
|
31f712749b | ||
|
|
bcdf83100f | ||
|
|
dc88ea0ed9 | ||
|
|
543457a1ef | ||
|
|
5d781d0114 | ||
|
|
45b1dabe0a | ||
|
|
ffea373f86 | ||
|
|
ba650de562 | ||
|
|
39233ff240 | ||
|
|
8950423418 | ||
|
|
760a2b9ff7 | ||
|
|
a13f488b84 | ||
|
|
09b1582704 | ||
|
|
61ab49a124 | ||
|
|
1a95b04d79 | ||
|
|
691a6b0a72 | ||
|
|
1d0a902251 | ||
|
|
1de9f5a2fb | ||
|
|
7a5342cc93 | ||
|
|
52c73eabeb | ||
|
|
02de1f0a1e | ||
|
|
3453bdf607 | ||
|
|
ac2723c3a0 | ||
|
|
1536c09df3 | ||
|
|
fade66afd9 | ||
|
|
f63484d0b0 | ||
|
|
3543e96ec5 | ||
|
|
c83ea0db4f | ||
|
|
ad256367be | ||
|
|
73b427370b | ||
|
|
ca7234e33f | ||
|
|
cd7b267171 |
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/workflows/deploy-docs.yml
vendored
2
.github/workflows/deploy-docs.yml
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/workflows/devcontainer-build.yml
vendored
2
.github/workflows/devcontainer-build.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/docker-build.yml
vendored
4
.github/workflows/docker-build.yml
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/workflows/docker-update-readme.yml
vendored
2
.github/workflows/docker-update-readme.yml
vendored
@@ -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
|
||||
|
||||
|
||||
4
.github/workflows/pre-commit-update.yml
vendored
4
.github/workflows/pre-commit-update.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2025.11-dev"
|
||||
__version__ = "2025.12-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
[
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user