Compare commits

...

25 Commits

Author SHA1 Message Date
Axel CH
096202de77
Merge 6b8ca7217b into ae41ab101a 2024-09-15 22:13:36 +02:00
Matthias
ae41ab101a docs: remove skip_pair_validation - it's no longer used.
Some checks are pending
Build Documentation / Deploy Docs through mike (push) Waiting to run
2024-09-15 11:28:57 +02:00
Matthias
f4881e7c6f tests: Adjust tests for removed validate_pairlist functionality 2024-09-15 11:28:57 +02:00
Matthias
94ef4380d4 chore: remove validate_pairs from exchange class
Invalid pairs were filtered out before this was called in most cases.
in cases where it's not - regular pairlist-filtering provides proper warnings.
2024-09-15 11:28:57 +02:00
Matthias
7ebe1b8c14 chore: remove pointless validation
pairs are validated through expand_pairlist.
If they're not in markets, they'll no longer be in the
pairlist once this function function is hit.
2024-09-15 11:02:49 +02:00
Matthias
79020bba28 chore: Remove "prohibitedIn" check
it's only been used for bitrex, which does no longer exist.
apparently this was forgotten when decomissioning bittrex.
2024-09-15 10:49:26 +02:00
Matthias
95c250ebcc chore: add explaining comment 2024-09-15 10:37:28 +02:00
Matthias
bfb14614cc chore: enhance change with comment 2024-09-15 09:48:44 +02:00
Matthias
12299d4810 feat: staticPairlist to warn for invalid pairs
Warnings about invalid pairs were "covered" by the implicit
filtering of `expand_pairlist()`
2024-09-15 09:46:47 +02:00
Matthias
c67a9d4e84 docs: update pairlist creation docs 2024-09-15 09:29:45 +02:00
Axel-CH
6b8ca7217b flake8 fix and cleanup 2024-09-11 21:48:48 -04:00
Axel-CH
2811a470aa handle pre existing open order cancelation on trade exit 2024-09-11 21:11:20 -04:00
Axel-CH
1fccdd8cda fix test_exit_positions 2024-09-11 18:59:29 -04:00
Axel-CH
33b421014d remove irrelevant trade.has_open_orders conditions 2024-09-11 18:42:51 -04:00
Axel-CH
730bef2920 add comments and logs for failing test, create untied_assets propertyto Trade 2024-09-09 19:28:04 -04:00
Axel-CH
79ce1ddaef rollback on process_open_trade_positions. Adjust position only if there is no open order, change will be made on other PR 2024-09-09 13:30:07 -04:00
Axel-CH
714822c93c add ETC/BTC pair to conftest get_markets 2024-09-09 13:15:17 -04:00
Axel-CH
fb3787173f fix test_exit_positions_exception 2024-09-09 12:07:50 -04:00
Axel-CH
e9ba0d2ce8 Merge remote-tracking branch 'origin/develop' into feature/proceed-exit-while-open-order 2024-09-06 17:29:07 -04:00
Axel-CH
6a580176ea Merge branch 'develop' into feature/proceed-exit-while-open-order 2024-07-01 17:49:40 -04:00
Axel-CH
910b3ad536 allow adjust trade position, even if there is an open order 2024-04-15 15:40:57 -04:00
Axel-CH
05cf4cab8e add has_open_entry_orders property to trade 2024-04-15 15:18:39 -04:00
Axel-CH
6752c3e288 add has_untied_assets, replace one has_open_orders condition by has_untied_assets in exit_positions 2024-04-15 14:43:40 -04:00
Axel-CH
faeda2a166 fix mypy error on has_open_position function 2024-04-14 00:40:12 -04:00
Axel-CH
3d67e0893a edit backtest_loop to check exit if trade has open position 2024-04-14 00:25:40 -04:00
14 changed files with 153 additions and 254 deletions

View File

@ -222,7 +222,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation) <br> **Datatype:** Dict
| `exchange.enable_ws` | Enable the usage of Websockets for the exchange. <br>[More information](#consuming-exchange-websockets).<br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean

View File

@ -205,7 +205,7 @@ This is called with each iteration of the bot (only if the Pairlist Handler is a
It must return the resulting pairlist (which may then be passed into the chain of Pairlist Handlers).
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the end-result is not shorter than expected.
Validations are optional, the parent class exposes a `verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the end-result is not shorter than expected.
#### filter_pairlist
@ -219,7 +219,7 @@ The default implementation in the base class simply calls the `_validate_pair()`
If overridden, it must return the resulting pairlist (which may then be passed into the next Pairlist Handler in the chain).
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the end result is not shorter than expected.
Validations are optional, the parent class exposes a `verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the end result is not shorter than expected.
In `VolumePairList`, this implements different methods of sorting, does early validation so only the expected number of pairs is returned.

View File

@ -55,7 +55,6 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis
By default, only currently enabled pairs are allowed.
To skip pair validation against active markets, set `"allow_inactive": true` within the `StaticPairList` configuration.
This can be useful for backtesting expired pairs (like quarterly spot-markets).
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
When used in a "follow-up" position (e.g. after VolumePairlist), all pairs in `'pair_whitelist'` will be added to the end of the pairlist.

View File

@ -610,9 +610,6 @@ def download_data_main(config: Config) -> None:
if "timeframes" not in config:
config["timeframes"] = DL_DATA_TIMEFRAMES
# Manual validations of relevant settings
if not config["exchange"].get("skip_pair_validation", False):
exchange.validate_pairs(expanded_pairs)
logger.info(
f"About to download pairs: {expanded_pairs}, "
f"intervals: {config['timeframes']} to {config['datadir']}"

View File

@ -104,7 +104,6 @@ from freqtrade.misc import (
file_load_json,
safe_value_fallback2,
)
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.util import dt_from_ts, dt_now
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time
from freqtrade.util.periodic_cache import PeriodicCache
@ -331,8 +330,6 @@ class Exchange:
# Check if all pairs are available
self.validate_stakecurrency(config["stake_currency"])
if not config["exchange"].get("skip_pair_validation"):
self.validate_pairs(config["exchange"]["pair_whitelist"])
self.validate_ordertypes(config.get("order_types", {}))
self.validate_order_time_in_force(config.get("order_time_in_force", {}))
self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
@ -702,54 +699,6 @@ class Exchange:
f"Available currencies are: {', '.join(quote_currencies)}"
)
def validate_pairs(self, pairs: List[str]) -> None:
"""
Checks if all given pairs are tradable on the current exchange.
:param pairs: list of pairs
:raise: OperationalException if one pair is not available
:return: None
"""
if not self.markets:
logger.warning("Unable to validate pairs (assuming they are correct).")
return
extended_pairs = expand_pairlist(pairs, list(self.markets), keep_invalid=True)
invalid_pairs = []
for pair in extended_pairs:
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
if self.markets and pair not in self.markets:
raise OperationalException(
f"Pair {pair} is not available on {self.name} {self.trading_mode}. "
f"Please remove {pair} from your whitelist."
)
# From ccxt Documentation:
# markets.info: An associative array of non-common market properties,
# including fees, rates, limits and other general market information.
# The internal info array is different for each particular market,
# its contents depend on the exchange.
# It can also be a string or similar ... so we need to verify that first.
elif isinstance(self.markets[pair].get("info"), dict) and self.markets[pair].get(
"info", {}
).get("prohibitedIn", False):
# Warn users about restricted pairs in whitelist.
# We cannot determine reliably if Users are affected.
logger.warning(
f"Pair {pair} is restricted for some users on this exchange."
f"Please check if you are impacted by this restriction "
f"on the exchange and eventually remove {pair} from your whitelist."
)
if (
self._config["stake_currency"]
and self.get_pair_quote_currency(pair) != self._config["stake_currency"]
):
invalid_pairs.append(pair)
if invalid_pairs:
raise OperationalException(
f"Stake-currency '{self._config['stake_currency']}' not compatible with "
f"pair-whitelist. Please remove the following pairs: {invalid_pairs}"
)
def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
"""
Get valid pair combination of curr_1 and curr_2 by trying both combinations.

View File

@ -715,8 +715,6 @@ class FreqtradeBot(LoggingMixin):
"""
# Walk through each pair and check if it needs changes
for trade in Trade.get_open_trades():
# If there is any open orders, wait for them to finish.
# TODO Remove to allow mul open orders
if not trade.has_open_orders:
# Do a wallets update (will be ratelimited to once per hour)
self.wallets.update(False)
@ -724,8 +722,7 @@ class FreqtradeBot(LoggingMixin):
self.check_and_call_adjust_trade_position(trade)
except DependencyException as exception:
logger.warning(
f"Unable to adjust position of trade for {trade.pair}: {exception}"
)
f"Unable to adjust position of trade for {trade.pair}: {exception}")
def check_and_call_adjust_trade_position(self, trade: Trade):
"""
@ -1251,8 +1248,7 @@ class FreqtradeBot(LoggingMixin):
trades_closed = 0
for trade in trades:
if (
not trade.has_open_orders
and not trade.has_open_sl_orders
not trade.has_open_sl_orders
and not self.wallets.check_exit_amount(trade)
):
logger.warning(
@ -1277,7 +1273,7 @@ class FreqtradeBot(LoggingMixin):
f"Unable to handle stoploss on exchange for {trade.pair}: {exception}"
)
# Check if we can sell our current pair
if not trade.has_open_orders and trade.is_open and self.handle_trade(trade):
if trade.is_open and self.handle_trade(trade):
trades_closed += 1
except DependencyException as exception:
@ -1421,7 +1417,7 @@ class FreqtradeBot(LoggingMixin):
self.handle_protections(trade.pair, trade.trade_direction)
return True
if trade.has_open_orders or not trade.is_open:
if not trade.is_open:
# Trade has an open order, Stoploss-handling can't happen in this case
# as the Amount on the exchange is tied up in another trade.
# The trade can be closed already (sell-order fill confirmation came in this iteration)
@ -1689,13 +1685,15 @@ class FreqtradeBot(LoggingMixin):
logger.warning(f"Unable to replace order for {trade.pair}: {exception}")
self.replace_order_failed(trade, f"Could not replace order for {trade}.")
def cancel_all_open_orders(self) -> None:
def cancel_open_orders_of_trade(self, trade: Trade, reason: str, sides: List[str]) -> None:
"""
Cancel all orders that are currently open
Cancel trade orders of specified sides that are currently open
:param trade: Trade object of the trade we're analyzing
:param reason: The reason for that cancelation
:param sides: The sides where cancellation should take place
:return: None
"""
for trade in Trade.get_open_trades():
for open_order in trade.open_orders:
try:
order = self.exchange.fetch_order(open_order.order_id, trade.pair)
@ -1703,15 +1701,30 @@ class FreqtradeBot(LoggingMixin):
logger.info("Can't query order for %s due to %s", trade, traceback.format_exc())
continue
for side in sides:
if (order["side"] == side):
if order["side"] == trade.entry_side:
self.handle_cancel_enter(
trade, order, open_order, constants.CANCEL_REASON["ALL_CANCELLED"]
trade, order, open_order, reason
)
elif order["side"] == trade.exit_side:
self.handle_cancel_exit(
trade, order, open_order, constants.CANCEL_REASON["ALL_CANCELLED"]
trade, order, open_order, reason
)
def cancel_all_open_orders(self) -> None:
"""
Cancel all orders that are currently open
:return: None
"""
for trade in Trade.get_open_trades():
self.cancel_open_orders_of_trade(
trade, constants.CANCEL_REASON["ALL_CANCELLED"],
[trade.entry_side, trade.exit_side]
)
Trade.commit()
def handle_cancel_enter(
@ -1957,6 +1970,14 @@ class FreqtradeBot(LoggingMixin):
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
if trade.has_open_orders:
# cancel any open order of this trade
self.cancel_open_orders_of_trade(
trade, constants.CANCEL_REASON["REPLACE"],
[trade.exit_side]
)
Trade.commit()
# First cancelling stoploss on exchange ...
trade = self.cancel_stoploss_on_exchange(trade)

View File

@ -1376,7 +1376,6 @@ class Backtesting:
self.wallets.update()
# 4. Create exit orders (if any)
if not trade.has_open_orders:
self._check_trade_exit(trade, row, current_time) # Place exit order if necessary
# 5. Process exit orders.

View File

@ -178,6 +178,7 @@ class Order(ModelBase):
return (
f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, "
f"amount={self.amount}, "
f"status={self.status}, date={self.order_date_utc:{DATETIME_PRINT_FORMAT}})"
)
@ -585,6 +586,67 @@ class LocalTrade:
]
return len(open_orders_wo_sl) > 0
@property
def has_open_entry_orders(self) -> bool:
"""
True if there are open entry orders for this trade
"""
open_entry_orders = [
o for o in self.orders
if o.ft_order_side == self.entry_side and o.ft_is_open
]
return len(open_entry_orders) > 0
@property
def has_open_position(self) -> bool:
"""
True if there is an open position for this trade
"""
entry_orders = [
o for o in self.orders
if o.ft_order_side == self.entry_side
]
entry_orders_filled_qty = sum(eno.safe_filled for eno in entry_orders)
exit_orders = [
o for o in self.orders
if o.ft_order_side == self.exit_side
]
exit_orders_filled_qty = sum(exo.safe_filled for exo in exit_orders)
return (entry_orders_filled_qty - exit_orders_filled_qty) > 0
@property
def untied_assets(self) -> float:
entry_orders = [
o for o in self.orders
if o.ft_order_side == self.entry_side
]
entry_orders_filled_qty = sum(eno.safe_filled for eno in entry_orders)
exit_orders = [
o for o in self.orders
if o.ft_order_side == self.exit_side
]
exit_orders_remaining_qty = sum(exo.safe_remaining for exo in exit_orders)
untied_remaining = entry_orders_filled_qty - exit_orders_remaining_qty
logger.info(f"entry_orders: {entry_orders}")
logger.info(f"exit_orders: {exit_orders}")
logger.info(f"entry_orders_filled_qty: {entry_orders_filled_qty}")
logger.info(f"exit_orders_remaining_qty: {exit_orders_remaining_qty}")
logger.info(f"untied_remaining: {untied_remaining}")
return untied_remaining
@property
def has_untied_assets(self) -> bool:
"""
True if there is still remaining position not yet tied up to exit order
"""
return self.untied_assets > 0
@property
def open_sl_orders(self) -> List[Order]:
"""

View File

@ -61,14 +61,15 @@ class StaticPairList(IPairList):
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: List of pairs
"""
if self._allow_inactive:
return self.verify_whitelist(
wl = self.verify_whitelist(
self._config["exchange"]["pair_whitelist"], logger.info, keep_invalid=True
)
if self._allow_inactive:
return wl
else:
return self._whitelist_for_active_markets(
self.verify_whitelist(self._config["exchange"]["pair_whitelist"], logger.info)
)
# Avoid implicit filtering of "verify_whitelist" to keep
# proper warnings in the log
return self._whitelist_for_active_markets(wl)
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""

View File

@ -28,6 +28,7 @@ def expand_pairlist(
except re.error as err:
raise ValueError(f"Wildcard error in {pair_wc}, {err}")
# Remove wildcard pairs that didn't have a match.
result = [element for element in result if re.fullmatch(r"^[A-Za-z0-9:/-]+$", element)]
else:

View File

@ -964,6 +964,29 @@ def get_markets():
},
"info": {},
},
"ETC/BTC": {
"id": "ETCBTC",
"symbol": "ETC/BTC",
"base": "ETC",
"quote": "BTC",
"active": True,
"spot": True,
"swap": False,
"linear": None,
"type": "spot",
"contractSize": None,
"precision": {"base": 8, "quote": 8, "amount": 2, "price": 7},
"limits": {
"amount": {"min": 0.01, "max": 90000000.0},
"price": {"min": 1e-07, "max": 1000.0},
"cost": {"min": 0.0001, "max": 9000000.0},
"leverage": {
"min": None,
"max": None,
},
},
"info": {},
},
"ETH/USDT": {
"id": "USDT-ETH",
"symbol": "ETH/USDT",

View File

@ -255,7 +255,6 @@ def test_init_exception(default_conf, mocker):
def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=MagicMock()))
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@ -555,7 +554,6 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
def test__load_async_markets(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency")
@ -584,7 +582,6 @@ def test__load_markets(default_conf, mocker, caplog):
api_mock = MagicMock()
api_mock.load_markets = get_mock_coro(side_effect=ccxt.BaseError("SomeError"))
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@ -684,7 +681,6 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
@ -702,7 +698,6 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog):
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
with pytest.raises(
ConfigurationError,
@ -755,147 +750,6 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
assert ex.get_pair_base_currency(pair) == expected
def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock()
id_mock = PropertyMock(return_value="test_exchange")
type(api_mock).id = id_mock
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(
f"{EXMS}._load_async_markets",
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
},
)
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
# test exchange.validate_pairs directly
# No assert - but this should not fail (!)
Exchange(default_conf)
def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(
return_value={"XRP/BTC": {"inactive": True, "base": "XRP", "quote": "BTC"}}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}._load_async_markets")
with pytest.raises(OperationalException, match=r"not available"):
Exchange(default_conf)
def test_validate_pairs_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
api_mock = MagicMock()
mocker.patch(f"{EXMS}.name", PropertyMock(return_value="Binance"))
type(api_mock).markets = PropertyMock(return_value={})
mocker.patch(f"{EXMS}._init_ccxt", api_mock)
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}._load_async_markets")
with pytest.raises(OperationalException, match=r"Pair ETH/BTC is not available on Binance"):
Exchange(default_conf)
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value={}))
Exchange(default_conf)
assert log_has("Unable to validate pairs (assuming they are correct).", caplog)
def test_validate_pairs_restricted(default_conf, mocker, caplog):
api_mock = MagicMock()
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC", "info": {"prohibitedIn": ["US"]}},
"NEO/BTC": {"quote": "BTC", "info": "TestString"}, # info can also be a string ...
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency")
Exchange(default_conf)
assert log_has(
"Pair XRP/BTC is restricted for some users on this exchange."
"Please check if you are impacted by this restriction "
"on the exchange and eventually remove XRP/BTC from your whitelist.",
caplog,
)
def test_validate_pairs_stakecompatibility(default_conf, mocker):
api_mock = MagicMock()
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
"HELLO-WORLD": {"quote": "BTC"},
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker):
api_mock = MagicMock()
default_conf["stake_currency"] = ""
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
"HELLO-WORLD": {"quote": "BTC"},
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
assert type(api_mock).load_markets.call_count == 1
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker):
default_conf["exchange"]["pair_whitelist"].append("HELLO-WORLD")
api_mock = MagicMock()
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
"HELLO-WORLD": {"quote": "USDT"},
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"):
Exchange(default_conf)
@pytest.mark.parametrize("timeframe", [("5m"), ("1m"), ("15m"), ("1h")])
def test_validate_timeframes(default_conf, mocker, timeframe):
default_conf["timeframe"] = timeframe
@ -907,7 +761,6 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
@ -925,7 +778,6 @@ def test_validate_timeframes_failed(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
with pytest.raises(
@ -955,7 +807,6 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(
OperationalException,
@ -977,7 +828,6 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs", MagicMock())
mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(
OperationalException,
@ -999,7 +849,6 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_required_startup_candles")
@ -1016,7 +865,6 @@ def test_validate_pricing(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.name", "Binance")
@ -1051,7 +899,6 @@ def test_validate_ordertypes(default_conf, mocker):
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@ -1110,7 +957,6 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@ -1135,7 +981,6 @@ def test_validate_order_types_not_in_config(default_conf, mocker):
api_mock = MagicMock()
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency")
@ -1151,7 +996,6 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt", api_mock)
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency")
@ -4185,7 +4029,6 @@ def test_merge_ft_has_dict(default_conf, mocker):
EXMS,
_init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(),
validate_pairs=MagicMock(),
validate_timeframes=MagicMock(),
validate_stakecurrency=MagicMock(),
validate_pricing=MagicMock(),
@ -4220,7 +4063,6 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
EXMS,
_init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(),
validate_pairs=MagicMock(),
validate_timeframes=MagicMock(),
validate_pricing=MagicMock(),
markets=PropertyMock(return_value=markets),
@ -4500,7 +4342,6 @@ def test_get_markets(
EXMS,
_init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(),
validate_pairs=MagicMock(),
validate_timeframes=MagicMock(),
validate_pricing=MagicMock(),
markets=PropertyMock(return_value=markets_static),

View File

@ -1265,14 +1265,14 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
trades = [trade]
freqtrade.wallets.update()
n = freqtrade.exit_positions(trades)
assert n == 0
assert n == 1
# Test amount not modified by fee-logic
assert not log_has_re(r"Applying fee to amount for Trade .*", caplog)
gra = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", return_value=0.0)
# test amount modified by fee-logic
n = freqtrade.exit_positions(trades)
assert n == 0
assert n == 1
assert gra.call_count == 0
@ -1305,6 +1305,7 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog
ft_price=trade.open_rate,
order_id=order_id,
ft_is_open=False,
filled=11
)
)
Trade.session.add(trade)
@ -2204,7 +2205,6 @@ def test_manage_open_orders_buy_exception(
patch_exchange(mocker)
mocker.patch.multiple(
EXMS,
validate_pairs=MagicMock(),
fetch_ticker=ticker_usdt,
fetch_order=MagicMock(side_effect=ExchangeError),
cancel_order=cancel_order_mock,

View File

@ -1,3 +1,4 @@
import logging
import time
from unittest.mock import MagicMock
@ -347,8 +348,8 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert trade.nr_of_successful_exits == 1
@pytest.mark.parametrize("leverage", [1, 2])
def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) -> None:
@pytest.mark.parametrize("leverage", [1])
def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker, caplog) -> None:
default_conf_usdt["position_adjustment_enable"] = True
default_conf_usdt["trading_mode"] = "futures"
default_conf_usdt["margin_mode"] = "isolated"
@ -478,10 +479,16 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert pytest.approx(trade.amount) == 91.689215 * leverage
assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage
assert freqtrade.strategy.adjust_entry_price.call_count == 0
caplog.clear()
caplog.set_level(logging.DEBUG)
# Process again, should not adjust entry price
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 5
assert trade.orders[-2].status == "canceled"
assert len(trade.orders) == 6
assert trade.orders[-1].side == trade.exit_side
assert trade.orders[-1].status == "open"
assert trade.orders[-1].price == 2.02
# Adjust entry price cannot be called - this is an exit order