Compare commits

...

34 Commits

Author SHA1 Message Date
Matthias
6848f9197e Merge pull request #12613 from freqtrade/fix/binance
Fix binance futures stoploss Order handling
2025-12-13 08:28:21 +01:00
Matthias
6d2c30abca chore: bump ccxt to 4.5.27 2025-12-12 06:54:29 +01:00
Matthias
93bde7dc46 fix: support binance algo orders
closes #12610
2025-12-12 06:52:11 +01:00
Matthias
97e2e0a405 chore: remove hard-pin of pycares 2025-12-11 18:23:58 +01:00
Matthias
12206f028b chore: comment wording improvements 2025-12-11 07:16:36 +01:00
Matthias
4c7944ac77 chore: update comment wording 2025-12-11 07:09:23 +01:00
Matthias
d4ced7b416 docs: improve doc wording 2025-12-11 07:05:58 +01:00
Matthias
451eef5c99 test: further test simplifications 2025-12-11 06:49:56 +01:00
Matthias
ae8f059de0 Merge pull request #12611 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-12-11 06:49:18 +01:00
Matthias
878bd7cbc7 chore: pin pycares for now 2025-12-11 06:35:27 +01:00
Freqtrade Bot
6d017c9a6c chore: update pre-commit hooks 2025-12-11 03:29:24 +00:00
Matthias
4c3d9b8c70 test: simplify some stoploss test setups 2025-12-10 19:28:30 +01:00
Matthias
23a4260859 chore: simplify okx cancel stoploss method 2025-12-10 17:23:10 +01:00
Matthias
5919736904 Merge pull request #12607 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-12-09 07:23:50 +01:00
Freqtrade Bot
732610e200 chore: update pre-commit hooks 2025-12-09 03:27:07 +00:00
Matthias
3689b52309 docs: add Section about very loose stoploss on exchange
closes #12598
2025-12-08 17:21:51 +01:00
Matthias
01fbf31405 chore: don't suggest binance.us supports futures
it doesn't.
2025-12-08 17:15:41 +01:00
Matthias
1e187e0945 Merge pull request #12604 from freqtrade/dependabot/pip/develop/ccxt-4.5.25
chore(deps): bump ccxt from 4.5.24 to 4.5.26
2025-12-08 11:52:15 +01:00
dependabot[bot]
e4fc5df1cf chore(deps): bump ccxt from 4.5.24 to 4.5.25
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.5.24 to 4.5.25.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.5.24...v4.5.25)

---
updated-dependencies:
- dependency-name: ccxt
  dependency-version: 4.5.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 09:25:30 +00:00
Matthias
253950deb6 Merge pull request #12600 from freqtrade/dependabot/pip/develop/scipy-ea2b5522bf
chore(deps-dev): bump scipy-stubs from 1.16.3.1 to 1.16.3.2 in the scipy group
2025-12-08 08:38:40 +01:00
Matthias
ed92d6beb9 Merge pull request #12601 from freqtrade/dependabot/pip/develop/ruff-0.14.7
chore(deps-dev): bump ruff from 0.14.6 to 0.14.7
2025-12-08 08:27:20 +01:00
Matthias
ebb362d9fa chore: bump scipy-stubs in pre-commit config 2025-12-08 08:16:30 +01:00
dependabot[bot]
f23fad420e chore(deps-dev): bump ruff from 0.14.6 to 0.14.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.6 to 0.14.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.6...0.14.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 07:11:06 +00:00
Matthias
aaf23221ff Merge pull request #12602 from freqtrade/dependabot/pip/develop/fastapi-0.123.0
chore(deps): bump fastapi from 0.122.0 to 0.123.0
2025-12-08 08:10:21 +01:00
Matthias
156c1a99a9 Merge pull request #12603 from freqtrade/dependabot/pip/develop/mypy-1.19.0
chore(deps-dev): bump mypy from 1.18.2 to 1.19.0
2025-12-08 08:09:35 +01:00
dependabot[bot]
cb55ef5c59 chore(deps-dev): bump mypy from 1.18.2 to 1.19.0
Bumps [mypy](https://github.com/python/mypy) from 1.18.2 to 1.19.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.18.2...v1.19.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.19.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 03:02:14 +00:00
dependabot[bot]
6540fbb8e7 chore(deps): bump fastapi from 0.122.0 to 0.123.0
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.122.0 to 0.123.0.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.122.0...0.123.0)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.123.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 03:02:07 +00:00
dependabot[bot]
412392aea9 chore(deps-dev): bump scipy-stubs in the scipy group
Bumps the scipy group with 1 update: [scipy-stubs](https://github.com/scipy/scipy-stubs).


Updates `scipy-stubs` from 1.16.3.1 to 1.16.3.2
- [Release notes](https://github.com/scipy/scipy-stubs/releases)
- [Commits](https://github.com/scipy/scipy-stubs/compare/v1.16.3.1...v1.16.3.2)

---
updated-dependencies:
- dependency-name: scipy-stubs
  dependency-version: 1.16.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: scipy
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 03:01:42 +00:00
Matthias
e3229935f6 Merge pull request #12594 from stremblayiOS/fix-hyperliquid-fetch-positions
Fix IndexError in fetch_positions for Hyperliquid when no pair specified
2025-12-05 20:16:48 +01:00
Matthias
b1ee115b77 Merge pull request #12593 from arawrdn/develop
Update README.md
2025-12-05 19:53:33 +01:00
Matthias
d6060f04bc Merge pull request #12595 from freqtrade/dependabot/pip/urllib3-2.6.0
chore(deps): bump urllib3 from 2.5.0 to 2.6.0
2025-12-05 19:43:22 +01:00
dependabot[bot]
1ae5310d2f chore(deps): bump urllib3 from 2.5.0 to 2.6.0
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.5.0...2.6.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 18:27:27 +00:00
stremblayiOS
417a0817a7 Fix IndexError in fetch_positions for Hyperliquid when no pair specified
## Summary

Fix IndexError crash in fetch_positions() when initializing wallets on Hyperliquid exchange.

## Quick changelog

- Changed fetch_positions to pass None instead of empty list when no specific pair is requested
- Fixes compatibility with Hyperliquid CCXT implementation that expects None for all positions

## What's new?

When fetch_positions() is called without a specific pair parameter, the code was passing an empty list [] to the CCXT API.
For Hyperliquid exchange, this causes an IndexError because the exchange's implementation attempts to access symbols[0]
without checking if the list is empty.

The CCXT standard is to pass None (not an empty list) when requesting all positions. This change aligns the code with
the CCXT API convention and prevents the crash on Hyperliquid during wallet initialization.

Error that was occurring:
```
IndexError: list index out of range
  at /root/freqtrade/.venv/lib/python3.11/site-packages/ccxt/hyperliquid.py:3051
  market = self.market(symbols[0])
```

This change does not use AI-generated code.
2025-12-05 18:33:40 +01:00
0xward
8ca25b1757 Update README.md
docs: fix minor typo and inconsistency in README disclaimer (Dry-run to dry-run)
2025-12-05 09:50:16 +07:00
18 changed files with 1601 additions and 2003 deletions

View File

@@ -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.1
- scipy-stubs==1.16.3.2
- SQLAlchemy==2.0.44
# stages: [push]
@@ -44,7 +44,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.14.7'
rev: 'v0.14.8'
hooks:
- id: ruff
- id: ruff-format

View File

@@ -15,7 +15,7 @@ This software is for educational purposes only. Do not risk money which
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
Always start by running a trading bot in Dry-run and do not engage money
Always start by running a trading bot in Dry-Run and do not engage money
before you understand how it works and what profit/loss you should
expect.
@@ -24,7 +24,7 @@ hesitate to read the source code and understand the mechanism of this bot.
## Supported Exchange marketplaces
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
Please read the [exchange-specific notes](docs/exchanges.md) to learn about special configurations that maybe needed for each exchange.
- [X] [Binance](https://www.binance.com/)
- [X] [BingX](https://bingx.com/invite/0EM9RX)

View File

@@ -31,9 +31,14 @@ The Order-type will be ignored if only one mode is available.
--8<-- "includes/exchange-features.md"
!!! Note "Tight stoploss"
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>
Do not set too low/tight stoploss value when using stop loss on exchange!
If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work.
!!! Warning "Loose stoploss"
Using stoploss on exchange with a very wide stoploss (e.g. -1) may fail to place the stoploss order on exchange due to exchange limitations.
In that case, the bot will fallback to using the `emergency_exit` order type to place a market order as placing the stoploss order failed.
Freqtrade currently does not implement a limitation to avoid this situation, so please ensure your stoploss values are within reasonable limits for your exchange or disable stoploss on exchange.
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
Enable or Disable stop loss on exchange.

View File

@@ -644,7 +644,7 @@ Each of these methods are called right before placing an order on the exchange.
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
!!! Note
Using custom_entry_price, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
When using `custom_entry_price()`, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
### Custom order entry and exit price example

View File

@@ -4,7 +4,7 @@ from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
from freqtrade.exchange.exchange import Exchange
# isort: on
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.binance import Binance, Binanceus, Binanceusdm
from freqtrade.exchange.bingx import Bingx
from freqtrade.exchange.bitget import Bitget
from freqtrade.exchange.bitmart import Bitmart

View File

@@ -17,7 +17,7 @@ from freqtrade.exchange.binance_public_data import (
download_archive_trades,
)
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import FtHas, Tickers
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas, Tickers
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.util import FtTTLCache
@@ -145,6 +145,20 @@ class Binance(Exchange):
except ccxt.BaseError as e:
raise OperationalException(e) from e
def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
if self.trading_mode == TradingMode.FUTURES:
params = params or {}
params.update({"stop": True})
return self.fetch_order(order_id, pair, params)
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
if self.trading_mode == TradingMode.FUTURES:
params = params or {}
params.update({"stop": True})
return self.cancel_order(order_id=order_id, pair=pair, params=params)
def get_historic_ohlcv(
self,
pair: str,
@@ -544,3 +558,26 @@ class Binance(Exchange):
cache[ft_symbol] = delist_dt
return cache.get(pair, None)
class Binanceusdm(Binance):
"""Binacne USDM Exchange
Same as Binance - only futures trading is supported (via ccxt).
Not actually necessary, binance should be preferred.
"""
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
class Binanceus(Binance):
"""Binance US exchange class.
Minimal adjustment to disable futures trading for the US subsidiary of Binance
"""
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.SPOT, MarginMode.NONE),
]

File diff suppressed because it is too large Load Diff

View File

@@ -45,8 +45,6 @@ BAD_EXCHANGES = {
}
MAP_EXCHANGE_CHILDCLASS = {
"binanceus": "binance",
"binanceusdm": "binance",
"okex": "okx",
"gateio": "gate",
"huboi": "htx",
@@ -54,6 +52,8 @@ MAP_EXCHANGE_CHILDCLASS = {
SUPPORTED_EXCHANGES = [
"binance",
"binanceus",
"binanceusdm",
"bingx",
"bitmart",
"bitget",

View File

@@ -1834,9 +1834,9 @@ class Exchange:
if self._config["dry_run"] or self.trading_mode != TradingMode.FUTURES:
return []
try:
symbols = []
symbols = None
if pair:
symbols.append(pair)
symbols = [pair]
positions: list[CcxtPosition] = self._api.fetch_positions(symbols)
self._log_exchange_response("fetch_positions", positions)
return positions

View File

@@ -266,14 +266,7 @@ class Okx(Exchange):
return order["id"]
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
params1 = {"stop": True}
# 'ordType': 'conditional'
#
return self.cancel_order(
order_id=order_id,
pair=pair,
params=params1,
)
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
orders = []

View File

@@ -2011,14 +2011,14 @@ class FreqtradeBot(LoggingMixin):
def _safe_exit_amount(self, trade: Trade, pair: str, amount: float) -> float:
"""
Get sellable amount.
Get exitable amount.
Should be trade.amount - but will fall back to the available amount if necessary.
This should cover cases where get_real_amount() was not able to update the amount
for whatever reason.
:param trade: Trade we're working with
:param pair: Pair we're trying to sell
:param pair: Pair we're trying to exit
:param amount: amount we expect to be available
:return: amount to sell
:return: amount to exit
:raise: DependencyException: if available balance is not within 2% of the available amount.
"""
# Update wallets to ensure amounts tied up in a stoploss is now free!
@@ -2058,7 +2058,7 @@ class FreqtradeBot(LoggingMixin):
"""
Executes a trade exit for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:param limit: limit rate for the exit order
:param exit_check: CheckTuple with signal and reason
:return: True if it succeeds False
"""
@@ -2101,7 +2101,7 @@ class FreqtradeBot(LoggingMixin):
order_type = ordertype or self.strategy.order_types[exit_type]
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
# Emergency sells (default to market!)
# Emergency exits (default to market!)
order_type = self.strategy.order_types.get("emergency_exit", "market")
amount = self._safe_exit_amount(trade, trade.pair, sub_trade_amt or trade.amount)
@@ -2130,7 +2130,7 @@ class FreqtradeBot(LoggingMixin):
return False
try:
# Execute sell and update trade record
# Execute exit and update trade record
order = self.exchange.create_order(
pair=trade.pair,
ordertype=order_type,
@@ -2157,7 +2157,7 @@ class FreqtradeBot(LoggingMixin):
trade.exit_reason = exit_reason
self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
# In case of market sell orders the order can be closed immediately
# In case of market exit orders the order can be closed immediately
if order.get("status", "unknown") in ("closed", "expired"):
self.update_trade_state(trade, order_obj.order_id, order)
Trade.commit()

View File

@@ -6,8 +6,8 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.14.6
mypy==1.18.2
ruff==0.14.7
mypy==1.19.0
pre-commit==4.5.0
pytest==9.0.1
pytest-asyncio==1.3.0
@@ -24,7 +24,7 @@ time-machine==3.1.0
nbconvert==7.16.6
# mypy types
scipy-stubs==1.16.3.1 # keep in sync with `scipy` in `requirements-hyperopt.txt`
scipy-stubs==1.16.3.2 # 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,7 +7,7 @@ ft-pandas-ta==0.3.16
ta-lib==0.6.8
technical==1.5.3
ccxt==4.5.24
ccxt==4.5.27
cryptography==46.0.3
aiohttp==3.13.2
SQLAlchemy==2.0.44
@@ -17,7 +17,7 @@ httpx>=0.24.1
humanize==4.14.0
cachetools==6.2.2
requests==2.32.5
urllib3==2.5.0
urllib3==2.6.0
certifi==2025.11.12
jsonschema==4.25.1
tabulate==0.9.0
@@ -37,7 +37,7 @@ orjson==3.11.4
sdnotify==0.3.2
# API Server
fastapi==0.122.0
fastapi==0.123.0
pydantic==2.12.5
uvicorn==0.38.0
pyjwt==2.10.1

View File

@@ -3901,37 +3901,29 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
default_conf["dry_run"] = False
mock_prefix = "freqtrade.exchange.gate.Gate"
if exchange_name == "okx":
mock_prefix = "freqtrade.exchange.okx.Okx"
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value={"for": 123})
mocker.patch(f"{mock_prefix}.fetch_stoploss_order", return_value={"for": 123})
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
mocker.patch.object(exchange, "fetch_stoploss_order", return_value={"for": 123})
res = {"fee": {}, "status": "canceled", "amount": 1234}
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=res)
mocker.patch(f"{mock_prefix}.cancel_stoploss_order", return_value=res)
mocker.patch.object(exchange, "cancel_stoploss_order", return_value=res)
co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555)
assert co == res
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value="canceled")
mocker.patch(f"{mock_prefix}.cancel_stoploss_order", return_value="canceled")
mocker.patch.object(exchange, "cancel_stoploss_order", return_value="canceled")
# Fall back to fetch_stoploss_order
co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555)
assert co == {"for": 123}
exc = InvalidOrderException("")
mocker.patch(f"{EXMS}.fetch_stoploss_order", side_effect=exc)
mocker.patch(f"{mock_prefix}.fetch_stoploss_order", side_effect=exc)
mocker.patch.object(exchange, "fetch_stoploss_order", side_effect=exc)
co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555)
assert co["amount"] == 555
assert co == {"id": "_", "fee": {}, "status": "canceled", "amount": 555, "info": {}}
with pytest.raises(InvalidOrderException):
exc = InvalidOrderException("Did not find order")
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=exc)
mocker.patch(f"{mock_prefix}.cancel_stoploss_order", side_effect=exc)
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
mocker.patch.object(exchange, "cancel_stoploss_order", side_effect=exc)
exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=123)
@@ -4116,7 +4108,7 @@ def test_fetch_order_or_stoploss_order(default_conf, mocker):
fetch_order_mock = MagicMock()
fetch_stoploss_order_mock = MagicMock()
mocker.patch.multiple(
EXMS,
exchange,
fetch_order=fetch_order_mock,
fetch_stoploss_order=fetch_stoploss_order_mock,
)

View File

@@ -60,13 +60,10 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
cancel_order_mock = MagicMock(side_effect=patch_stoploss)
mocker.patch.multiple(
EXMS,
create_stoploss=stoploss,
fetch_ticker=ticker,
get_fee=fee,
amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y,
fetch_stoploss_order=stoploss_order_mock,
cancel_stoploss_order_with_result=cancel_order_mock,
)
mocker.patch.multiple(
@@ -80,6 +77,12 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
mocker.patch("freqtrade.wallets.Wallets.check_exit_amount", return_value=True)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch.multiple(
freqtrade.exchange,
create_stoploss=stoploss,
fetch_stoploss_order=stoploss_order_mock,
cancel_stoploss_order_with_result=cancel_order_mock,
)
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
# Switch ordertype to market to close trade immediately
freqtrade.strategy.order_types["exit"] = "market"

View File

@@ -103,7 +103,7 @@ def test_handle_stoploss_on_exchange(
trade.is_open = True
hanging_stoploss_order = MagicMock(return_value={"id": "13434334", "status": "open"})
mocker.patch(f"{EXMS}.fetch_stoploss_order", hanging_stoploss_order)
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", hanging_stoploss_order)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
hanging_stoploss_order.assert_called_once_with("13434334", trade.pair)
@@ -116,7 +116,7 @@ def test_handle_stoploss_on_exchange(
trade.is_open = True
canceled_stoploss_order = MagicMock(return_value={"id": "13434334", "status": "canceled"})
mocker.patch(f"{EXMS}.fetch_stoploss_order", canceled_stoploss_order)
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", canceled_stoploss_order)
stoploss.reset_mock()
amount_before = trade.amount
@@ -149,7 +149,7 @@ def test_handle_stoploss_on_exchange(
"amount": enter_order["amount"],
}
)
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", stoploss_order_hit)
freqtrade.strategy.order_filled = MagicMock(return_value=None)
assert freqtrade.handle_stoploss_on_exchange(trade) is True
assert log_has_re(r"STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.", caplog)
@@ -158,7 +158,7 @@ def test_handle_stoploss_on_exchange(
assert freqtrade.strategy.order_filled.call_count == 1
caplog.clear()
mocker.patch(f"{EXMS}.create_stoploss", side_effect=ExchangeError())
mocker.patch.object(freqtrade.exchange, "create_stoploss", side_effect=ExchangeError())
trade.is_open = True
freqtrade.handle_stoploss_on_exchange(trade)
assert log_has("Unable to place a stoploss order on exchange.", caplog)
@@ -168,8 +168,13 @@ def test_handle_stoploss_on_exchange(
# It should try to add stoploss order
stop_order_dict.update({"id": "105"})
stoploss.reset_mock()
mocker.patch(f"{EXMS}.fetch_stoploss_order", side_effect=InvalidOrderException())
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
mocker.patch.multiple(
freqtrade.exchange,
fetch_stoploss_order=MagicMock(
side_effect=InvalidOrderException(),
),
create_stoploss=stoploss,
)
freqtrade.handle_stoploss_on_exchange(trade)
assert len(trade.open_sl_orders) == 1
assert stoploss.call_count == 1
@@ -179,8 +184,7 @@ def test_handle_stoploss_on_exchange(
trade.is_open = False
trade.open_sl_orders[-1].ft_is_open = False
stoploss.reset_mock()
mocker.patch(f"{EXMS}.fetch_order")
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
mocker.patch.multiple(freqtrade.exchange, fetch_order=MagicMock(), create_stoploss=stoploss)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.has_open_sl_orders is False
assert stoploss.call_count == 0
@@ -252,9 +256,12 @@ def test_handle_stoploss_on_exchange_emergency(
stoploss = MagicMock(side_effect=InvalidOrderException())
assert trade.has_open_sl_orders is True
Trade.commit()
mocker.patch(f"{EXMS}.cancel_stoploss_order_with_result", side_effect=InvalidOrderException())
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_cancelled)
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
mocker.patch.multiple(
freqtrade.exchange,
cancel_stoploss_order_with_result=MagicMock(side_effect=InvalidOrderException()),
fetch_stoploss_order=stoploss_order_cancelled,
create_stoploss=stoploss,
)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.has_open_sl_orders is False
assert trade.is_open is False
@@ -311,7 +318,7 @@ def test_handle_stoploss_on_exchange_partial(
"amount": enter_order["amount"],
}
)
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
mocker.patch.multiple(freqtrade.exchange, fetch_stoploss_order=stoploss_order_hit)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# Stoploss filled partially ...
assert trade.amount == 15
@@ -383,8 +390,11 @@ def test_handle_stoploss_on_exchange_partial_cancel_here(
"amount": enter_order["amount"],
}
)
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
mocker.patch(f"{EXMS}.cancel_stoploss_order_with_result", stoploss_order_cancel)
mocker.patch.multiple(
freqtrade.exchange,
fetch_stoploss_order=stoploss_order_hit,
cancel_stoploss_order_with_result=stoploss_order_cancel,
)
time_machine.shift(timedelta(minutes=15))
assert freqtrade.handle_stoploss_on_exchange(trade) is False
@@ -408,20 +418,20 @@ def test_handle_sle_cancel_cant_recreate(
mocker.patch.multiple(
EXMS,
fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}),
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf_usdt)
mocker.patch.multiple(
freqtrade.exchange,
create_order=MagicMock(
side_effect=[
enter_order,
exit_order,
]
),
get_fee=fee,
)
mocker.patch.multiple(
EXMS,
fetch_stoploss_order=MagicMock(return_value={"status": "canceled", "id": "100"}),
create_stoploss=MagicMock(side_effect=ExchangeError()),
)
freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
freqtrade.enter_positions()
@@ -644,8 +654,11 @@ def test_handle_stoploss_on_exchange_trailing(
stoploss_order_cancel = deepcopy(stoploss_order_hanging)
stoploss_order_cancel["status"] = "canceled"
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value=stoploss_order_hanging)
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=stoploss_order_cancel)
mocker.patch.multiple(
freqtrade.exchange,
fetch_stoploss_order=MagicMock(return_value=stoploss_order_hanging),
cancel_stoploss_order=MagicMock(return_value=stoploss_order_cancel),
)
# stoploss initially at 5%
assert freqtrade.handle_trade(trade) is False
@@ -671,9 +684,12 @@ def test_handle_stoploss_on_exchange_trailing(
return_value={"id": "13434334", "status": "canceled", "fee": {}, "amount": trade.amount}
)
stoploss_order_mock = MagicMock(return_value={"id": "so1", "status": "open"})
mocker.patch(f"{EXMS}.fetch_stoploss_order")
mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock)
mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock)
mocker.patch.multiple(
freqtrade.exchange,
fetch_stoploss_order=MagicMock(),
cancel_stoploss_order=cancel_order_mock,
create_stoploss=stoploss_order_mock,
)
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
@@ -711,8 +727,9 @@ def test_handle_stoploss_on_exchange_trailing(
}
),
)
mocker.patch(
f"{EXMS}.cancel_stoploss_order_with_result",
mocker.patch.object(
freqtrade.exchange,
"cancel_stoploss_order_with_result",
return_value={"id": "so1", "status": "canceled"},
)
assert len(trade.open_sl_orders) == 1
@@ -786,8 +803,12 @@ def test_handle_stoploss_on_exchange_trailing_error(
order_date=dt_now(),
)
)
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException())
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value=stoploss_order_hanging)
mocker.patch.object(
freqtrade.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException()
)
mocker.patch.object(
freqtrade.exchange, "fetch_stoploss_order", return_value=stoploss_order_hanging
)
time_machine.shift(timedelta(minutes=50))
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog)
@@ -799,8 +820,8 @@ def test_handle_stoploss_on_exchange_trailing_error(
# Fail creating stoploss order
caplog.clear()
cancel_mock = mocker.patch(f"{EXMS}.cancel_stoploss_order")
mocker.patch(f"{EXMS}.create_stoploss", side_effect=ExchangeError())
cancel_mock = mocker.patch.object(freqtrade.exchange, "cancel_stoploss_order")
mocker.patch.object(freqtrade.exchange, "create_stoploss", side_effect=ExchangeError())
time_machine.shift(timedelta(minutes=50))
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert cancel_mock.call_count == 2
@@ -846,20 +867,9 @@ def test_handle_stoploss_on_exchange_custom_stop(
mocker.patch.multiple(
EXMS,
fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}),
create_order=MagicMock(
side_effect=[
enter_order,
exit_order,
]
),
get_fee=fee,
is_cancel_order_result_suitable=MagicMock(return_value=True),
)
mocker.patch.multiple(
EXMS,
create_stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
)
# enabling TSL
default_conf_usdt["use_custom_stoploss"] = True
@@ -868,6 +878,17 @@ def test_handle_stoploss_on_exchange_custom_stop(
default_conf_usdt["minimal_roi"]["0"] = 999999999
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch.multiple(
freqtrade.exchange,
create_order=MagicMock(
side_effect=[
enter_order,
exit_order,
]
),
create_stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
)
# enabling stoploss on exchange
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
@@ -912,8 +933,11 @@ def test_handle_stoploss_on_exchange_custom_stop(
x["id"] = order_id
return x
mocker.patch(f"{EXMS}.fetch_stoploss_order", MagicMock(fetch_stoploss_order_mock))
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=slo_canceled)
mocker.patch.multiple(
freqtrade.exchange,
fetch_stoploss_order=MagicMock(fetch_stoploss_order_mock),
cancel_stoploss_order=MagicMock(return_value=slo_canceled),
)
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
@@ -932,8 +956,11 @@ def test_handle_stoploss_on_exchange_custom_stop(
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock(return_value={"id": "so1", "status": "open"})
mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock)
mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock)
mocker.patch.multiple(
freqtrade.exchange,
cancel_stoploss_order=cancel_order_mock,
create_stoploss=stoploss_order_mock,
)
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
@@ -1054,7 +1081,9 @@ def test_execute_trade_exit_sloe_cancel_exception(
mocker, default_conf_usdt, ticker_usdt, fee, caplog
) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException())
mocker.patch.object(
freqtrade.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException()
)
mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=300))
create_order_mock = MagicMock(
side_effect=[
@@ -1114,12 +1143,15 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
get_fee=fee,
amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y,
)
freqtrade = FreqtradeBot(default_conf_usdt)
mocker.patch.multiple(
freqtrade.exchange,
create_stoploss=stoploss,
cancel_stoploss_order=cancel_order,
_dry_is_price_crossed=MagicMock(side_effect=[True, False]),
)
freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
@@ -1208,7 +1240,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
"trades": None,
}
)
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_executed)
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", stoploss_executed)
freqtrade.exit_positions(trades)
assert trade.has_open_sl_orders is False

View File

@@ -386,11 +386,14 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
mocker.patch.multiple(
freqtradebot.exchange,
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)
freqtradebot.strategy.order_types["stoploss_on_exchange"] = True
create_mock_trades(fee, is_short)
rpc = RPC(freqtradebot)
@@ -426,13 +429,17 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
assert stoploss_mock.call_count == 1
assert res["cancel_order_count"] == 1
stoploss_mock = mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException)
stoploss_mock = mocker.patch.object(
freqtradebot.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException
)
res = rpc._rpc_delete("3")
assert stoploss_mock.call_count == 1
stoploss_mock.reset_mock()
cancel_mock = mocker.patch(f"{EXMS}.cancel_order", side_effect=InvalidOrderException)
cancel_mock = mocker.patch.object(
freqtradebot.exchange, "cancel_order", side_effect=InvalidOrderException
)
res = rpc._rpc_delete("4")
assert cancel_mock.call_count == 1

View File

@@ -1034,8 +1034,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
stoploss_mock = MagicMock()
cancel_mock = MagicMock()
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
ftbot.exchange,
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)