mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Compare commits
3 Commits
1feb11bac0
...
maint/pyth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0340d36af2 | ||
|
|
ad7816d51e | ||
|
|
73127c8179 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "ubuntu-22.04", "ubuntu-24.04", "macos-14", "macos-15" , "windows-2022", "windows-2025" ]
|
os: [ "ubuntu-22.04", "ubuntu-24.04", "macos-14", "macos-15" , "windows-2022", "windows-2025" ]
|
||||||
python-version: ["3.11", "3.12", "3.13"]
|
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Check for repository changes - Windows
|
- name: Check for repository changes - Windows
|
||||||
if: ${{ runner.os == 'Windows' && (matrix.python-version != '3.13') }}
|
if: ${{ runner.os == 'Windows' }}
|
||||||
run: |
|
run: |
|
||||||
if (git status --porcelain) {
|
if (git status --porcelain) {
|
||||||
Write-Host "Repository is dirty, changes detected:"
|
Write-Host "Repository is dirty, changes detected:"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ repos:
|
|||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.32.4.20250913
|
- types-requests==2.32.4.20250913
|
||||||
- types-tabulate==0.9.0.20241207
|
- types-tabulate==0.9.0.20241207
|
||||||
- types-python-dateutil==2.9.0.20251108
|
- types-python-dateutil==2.9.0.20251008
|
||||||
- scipy-stubs==1.16.3.0
|
- scipy-stubs==1.16.3.0
|
||||||
- SQLAlchemy==2.0.44
|
- SQLAlchemy==2.0.44
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
@@ -44,7 +44,7 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.14.5'
|
rev: 'v0.14.4'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import subprocess # noqa: S404, RUF100
|
import subprocess # noqa: S404, RUF100
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
@@ -63,9 +62,4 @@ def extract_command_partials():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if sys.version_info < (3, 13): # pragma: no cover
|
|
||||||
sys.exit(
|
|
||||||
"argparse output changed in Python 3.13+. "
|
|
||||||
"To keep command partials up to date, please run this script with Python 3.13+."
|
|
||||||
)
|
|
||||||
extract_command_partials()
|
extract_command_partials()
|
||||||
|
|||||||
@@ -2,14 +2,11 @@
|
|||||||
usage: freqtrade list-timeframes [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
usage: freqtrade list-timeframes [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||||
[-c PATH] [-d PATH] [--userdir PATH]
|
[-c PATH] [-d PATH] [--userdir PATH]
|
||||||
[--exchange EXCHANGE] [-1]
|
[--exchange EXCHANGE] [-1]
|
||||||
[--trading-mode {spot,margin,futures}]
|
|
||||||
|
|
||||||
options:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
-1, --one-column Print output in one column.
|
-1, --one-column Print output in one column.
|
||||||
--trading-mode, --tradingmode {spot,margin,futures}
|
|
||||||
Select Trading mode
|
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ This filter allows freqtrade to ignore pairs until they have been listed for at
|
|||||||
Removes pairs that will be delisted on the exchange maximum `max_days_from_now` days from now (defaults to `0` which remove all future delisted pairs no matter how far from now). Currently this filter only supports following exchanges:
|
Removes pairs that will be delisted on the exchange maximum `max_days_from_now` days from now (defaults to `0` which remove all future delisted pairs no matter how far from now). Currently this filter only supports following exchanges:
|
||||||
|
|
||||||
!!! Note "Available exchanges"
|
!!! Note "Available exchanges"
|
||||||
Delist filter is available on Bybit Futures, Bitget Futures and Binance, where Binance Futures will work for both dry and live modes, while Binance Spot is limited to live mode (for technical reasons).
|
Delist filter is only available on Binance, where Binance Futures will work for both dry and live modes, while Binance Spot is limited to live mode (for technical reasons).
|
||||||
|
|
||||||
!!! Warning "Backtesting"
|
!!! Warning "Backtesting"
|
||||||
`DelistFilter` does not support backtesting mode.
|
`DelistFilter` does not support backtesting mode.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
markdown==3.10
|
markdown==3.10
|
||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-material==9.7.0
|
mkdocs-material==9.6.23
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.17.1
|
pymdown-extensions==10.16.1
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
mike==2.1.3
|
mike==2.1.3
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ ARGS_BACKTEST_SHOW = [
|
|||||||
|
|
||||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
|
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
|
||||||
|
|
||||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column", "trading_mode"]
|
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||||
|
|
||||||
ARGS_LIST_PAIRS = [
|
ARGS_LIST_PAIRS = [
|
||||||
"exchange",
|
"exchange",
|
||||||
|
|||||||
@@ -143,20 +143,6 @@ def _calc_drawdown_series(
|
|||||||
max_drawdown_df["drawdown_relative"] = (
|
max_drawdown_df["drawdown_relative"] = (
|
||||||
max_drawdown_df["high_value"] - max_drawdown_df["cumulative"]
|
max_drawdown_df["high_value"] - max_drawdown_df["cumulative"]
|
||||||
) / max_drawdown_df["high_value"]
|
) / max_drawdown_df["high_value"]
|
||||||
|
|
||||||
# Add zero row at start to account for edge-cases with no winning / losing trades - so high/low
|
|
||||||
# will be 0.0 in such cases.
|
|
||||||
zero_row = pd.DataFrame(
|
|
||||||
{
|
|
||||||
"cumulative": [0.0],
|
|
||||||
"high_value": [0.0],
|
|
||||||
"drawdown": [0.0],
|
|
||||||
"drawdown_relative": [0.0],
|
|
||||||
"date": [profit_results.loc[0, date_col]],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
max_drawdown_df = pd.concat([zero_row, max_drawdown_df], ignore_index=True)
|
|
||||||
return max_drawdown_df
|
return max_drawdown_df
|
||||||
|
|
||||||
|
|
||||||
@@ -229,7 +215,6 @@ def calculate_max_drawdown(
|
|||||||
max_drawdown_df = _calc_drawdown_series(
|
max_drawdown_df = _calc_drawdown_series(
|
||||||
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
|
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
|
||||||
)
|
)
|
||||||
# max_drawdown_df has an extra zero row at the start
|
|
||||||
|
|
||||||
# Calculate maximum drawdown
|
# Calculate maximum drawdown
|
||||||
idxmin = (
|
idxmin = (
|
||||||
@@ -238,15 +223,15 @@ def calculate_max_drawdown(
|
|||||||
else max_drawdown_df["drawdown"].idxmin()
|
else max_drawdown_df["drawdown"].idxmin()
|
||||||
)
|
)
|
||||||
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
|
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
|
||||||
high_date = profit_results.at[max(high_idx - 1, 0), date_col]
|
high_date = profit_results.loc[high_idx, date_col]
|
||||||
low_date = profit_results.at[max(idxmin - 1, 0), date_col]
|
low_date = profit_results.loc[idxmin, date_col]
|
||||||
high_val = max_drawdown_df.at[high_idx, "cumulative"]
|
high_val = max_drawdown_df.loc[high_idx, "cumulative"]
|
||||||
low_val = max_drawdown_df.at[idxmin, "cumulative"]
|
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
|
||||||
max_drawdown_rel = max_drawdown_df.at[idxmin, "drawdown_relative"]
|
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
|
||||||
|
|
||||||
# Calculate current drawdown
|
# Calculate current drawdown
|
||||||
current_high_idx = max_drawdown_df["high_value"].iloc[:-1].idxmax()
|
current_high_idx = max_drawdown_df["high_value"].iloc[:-1].idxmax()
|
||||||
current_high_date = profit_results.at[max(current_high_idx - 1, 0), date_col]
|
current_high_date = profit_results.loc[current_high_idx, date_col]
|
||||||
current_high_value = max_drawdown_df.iloc[-1]["high_value"]
|
current_high_value = max_drawdown_df.iloc[-1]["high_value"]
|
||||||
current_cumulative = max_drawdown_df.iloc[-1]["cumulative"]
|
current_cumulative = max_drawdown_df.iloc[-1]["cumulative"]
|
||||||
current_drawdown_abs = current_high_value - current_cumulative
|
current_drawdown_abs = current_high_value - current_cumulative
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import (
|
from freqtrade.exceptions import (
|
||||||
DDosProtection,
|
DDosProtection,
|
||||||
OperationalException,
|
OperationalException,
|
||||||
@@ -14,7 +14,7 @@ from freqtrade.exceptions import (
|
|||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
|
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
|
||||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||||
from freqtrade.util import dt_from_ts, dt_now, dt_ts
|
from freqtrade.util.datetime_helpers import dt_now, dt_ts
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -37,7 +37,6 @@ class Bitget(Exchange):
|
|||||||
_ft_has_futures: FtHas = {
|
_ft_has_futures: FtHas = {
|
||||||
"mark_ohlcv_timeframe": "4h",
|
"mark_ohlcv_timeframe": "4h",
|
||||||
"funding_fee_candle_limit": 100,
|
"funding_fee_candle_limit": 100,
|
||||||
"has_delisting": True,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
@@ -237,35 +236,3 @@ class Bitget(Exchange):
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"Freqtrade currently only supports isolated futures for bitget"
|
"Freqtrade currently only supports isolated futures for bitget"
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_delisting_time(self, pair: str) -> datetime | None:
|
|
||||||
"""
|
|
||||||
Check if the pair gonna be delisted.
|
|
||||||
By default, it returns None.
|
|
||||||
:param pair: Market symbol
|
|
||||||
:return: Datetime if the pair gonna be delisted, None otherwise
|
|
||||||
"""
|
|
||||||
if self._config["runmode"] in OPTIMIZE_MODES:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
|
||||||
return self._check_delisting_futures(pair)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _check_delisting_futures(self, pair: str) -> datetime | None:
|
|
||||||
delivery_time = self.markets.get(pair, {}).get("info", {}).get("limitOpenTime", None)
|
|
||||||
if delivery_time:
|
|
||||||
if isinstance(delivery_time, str) and (delivery_time != ""):
|
|
||||||
delivery_time = int(delivery_time)
|
|
||||||
|
|
||||||
if not isinstance(delivery_time, int) or delivery_time <= 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
max_delivery = dt_ts() + (
|
|
||||||
14 * 24 * 60 * 60 * 1000
|
|
||||||
) # Assume exchange don't announce delisting more than 14 days in advance
|
|
||||||
|
|
||||||
if delivery_time < max_delivery:
|
|
||||||
return dt_from_ts(delivery_time)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ from datetime import datetime, timedelta
|
|||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import OPTIMIZE_MODES, MarginMode, PriceType, TradingMode
|
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||||
from freqtrade.misc import deep_merge_dicts
|
from freqtrade.misc import deep_merge_dicts
|
||||||
from freqtrade.util import dt_from_ts, dt_ts
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -55,7 +54,6 @@ class Bybit(Exchange):
|
|||||||
"exchange_has_overrides": {
|
"exchange_has_overrides": {
|
||||||
"fetchOrder": True,
|
"fetchOrder": True,
|
||||||
},
|
},
|
||||||
"has_delisting": True,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
@@ -296,35 +294,3 @@ class Bybit(Exchange):
|
|||||||
|
|
||||||
self.cache_leverage_tiers(tiers, self._config["stake_currency"])
|
self.cache_leverage_tiers(tiers, self._config["stake_currency"])
|
||||||
return tiers
|
return tiers
|
||||||
|
|
||||||
def check_delisting_time(self, pair: str) -> datetime | None:
|
|
||||||
"""
|
|
||||||
Check if the pair gonna be delisted.
|
|
||||||
By default, it returns None.
|
|
||||||
:param pair: Market symbol
|
|
||||||
:return: Datetime if the pair gonna be delisted, None otherwise
|
|
||||||
"""
|
|
||||||
if self._config["runmode"] in OPTIMIZE_MODES:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
|
||||||
return self._check_delisting_futures(pair)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _check_delisting_futures(self, pair: str) -> datetime | None:
|
|
||||||
delivery_time = self.markets.get(pair, {}).get("info", {}).get("deliveryTime", 0)
|
|
||||||
if delivery_time:
|
|
||||||
if isinstance(delivery_time, str) and (delivery_time != ""):
|
|
||||||
delivery_time = int(delivery_time)
|
|
||||||
|
|
||||||
if not isinstance(delivery_time, int) or delivery_time <= 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
max_delivery = dt_ts() + (
|
|
||||||
14 * 24 * 60 * 60 * 1000
|
|
||||||
) # Assume exchange don't announce delisting more than 14 days in advance
|
|
||||||
|
|
||||||
if delivery_time < max_delivery:
|
|
||||||
return dt_from_ts(delivery_time)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -430,15 +430,7 @@ class Exchange:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def timeframes(self) -> list[str]:
|
def timeframes(self) -> list[str]:
|
||||||
market_type = (
|
return list((self._api.timeframes or {}).keys())
|
||||||
"spot"
|
|
||||||
if self.trading_mode != TradingMode.FUTURES
|
|
||||||
else self._ft_has["ccxt_futures_name"]
|
|
||||||
)
|
|
||||||
timeframes = self._api.options.get("timeframes", {}).get(market_type)
|
|
||||||
if timeframes is None:
|
|
||||||
timeframes = self._api.timeframes
|
|
||||||
return list((timeframes or {}).keys())
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def markets(self) -> dict[str, Any]:
|
def markets(self) -> dict[str, Any]:
|
||||||
@@ -1303,7 +1295,7 @@ class Exchange:
|
|||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
def fetch_dry_run_order(self, order_id: str) -> CcxtOrder:
|
def fetch_dry_run_order(self, order_id) -> CcxtOrder:
|
||||||
"""
|
"""
|
||||||
Return dry-run order
|
Return dry-run order
|
||||||
Only call if running in dry-run mode.
|
Only call if running in dry-run mode.
|
||||||
@@ -1315,12 +1307,11 @@ class Exchange:
|
|||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
from freqtrade.persistence import Order
|
from freqtrade.persistence import Order
|
||||||
|
|
||||||
order_obj = Order.order_by_id(order_id)
|
order = Order.order_by_id(order_id)
|
||||||
if order_obj:
|
if order:
|
||||||
order = order_obj.to_ccxt_object(self._ft_has["stop_price_prop"])
|
ccxt_order = order.to_ccxt_object(self._ft_has["stop_price_prop"])
|
||||||
order = self.check_dry_limit_order_filled(order)
|
self._dry_run_open_orders[order_id] = ccxt_order
|
||||||
self._dry_run_open_orders[order_id] = order
|
return ccxt_order
|
||||||
return order
|
|
||||||
# Gracefully handle errors with dry-run orders.
|
# Gracefully handle errors with dry-run orders.
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
f"Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}"
|
f"Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}"
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ from freqtrade.rpc.rpc_types import (
|
|||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from freqtrade.util import FtPrecise, MeasureTime, PeriodicCache, dt_from_ts, dt_now
|
from freqtrade.util import FtPrecise, MeasureTime, PeriodicCache, dt_from_ts, dt_now
|
||||||
from freqtrade.util.migrations import migrate_live_content
|
from freqtrade.util.migrations.binance_mig import migrate_binance_futures_names
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
Called on startup and after reloading the bot - triggers notifications and
|
Called on startup and after reloading the bot - triggers notifications and
|
||||||
performs startup tasks
|
performs startup tasks
|
||||||
"""
|
"""
|
||||||
migrate_live_content(self.config, self.exchange)
|
migrate_binance_futures_names(self.config)
|
||||||
set_startup_time()
|
set_startup_time()
|
||||||
|
|
||||||
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ class Telegram(RPCHandler):
|
|||||||
# TODO: This calculation ignores fees.
|
# TODO: This calculation ignores fees.
|
||||||
price_to_1st_entry = (cur_entry_average - first_avg) / first_avg
|
price_to_1st_entry = (cur_entry_average - first_avg) / first_avg
|
||||||
if is_open:
|
if is_open:
|
||||||
lines.append(f"({dt_humanize_delta(order['order_filled_date'])})")
|
lines.append("({})".format(dt_humanize_delta(order["order_filled_date"])))
|
||||||
lines.append(
|
lines.append(
|
||||||
f"*Amount:* {round_value(cur_entry_amount, 8)} "
|
f"*Amount:* {round_value(cur_entry_amount, 8)} "
|
||||||
f"({fmt_coin(order['cost'], quote_currency)})"
|
f"({fmt_coin(order['cost'], quote_currency)})"
|
||||||
@@ -701,7 +701,7 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
|
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
|
||||||
for r in results:
|
for r in results:
|
||||||
lines = [f"*Order List for Trade #*`{r['trade_id']}`"]
|
lines = ["*Order List for Trade #*`{trade_id}`"]
|
||||||
|
|
||||||
lines_detail = self._prepare_order_details(
|
lines_detail = self._prepare_order_details(
|
||||||
r["orders"], r["quote_currency"], r["is_open"]
|
r["orders"], r["quote_currency"], r["is_open"]
|
||||||
@@ -720,10 +720,10 @@ class Telegram(RPCHandler):
|
|||||||
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
|
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
|
||||||
msg += line + "\n"
|
msg += line + "\n"
|
||||||
else:
|
else:
|
||||||
await self._send_msg(msg)
|
await self._send_msg(msg.format(**r))
|
||||||
msg = f"*Order List for Trade #*`{r['trade_id']}` - continued\n" + line + "\n"
|
msg = "*Order List for Trade #*`{trade_id}` - continued\n" + line + "\n"
|
||||||
|
|
||||||
await self._send_msg(msg)
|
await self._send_msg(msg.format(**r))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
async def _status(self, update: Update, context: CallbackContext) -> None:
|
async def _status(self, update: Update, context: CallbackContext) -> None:
|
||||||
@@ -774,25 +774,26 @@ class Telegram(RPCHandler):
|
|||||||
r["realized_profit_r"] = fmt_coin(r["realized_profit"], r["quote_currency"])
|
r["realized_profit_r"] = fmt_coin(r["realized_profit"], r["quote_currency"])
|
||||||
r["total_profit_abs_r"] = fmt_coin(r["total_profit_abs"], r["quote_currency"])
|
r["total_profit_abs_r"] = fmt_coin(r["total_profit_abs"], r["quote_currency"])
|
||||||
lines = [
|
lines = [
|
||||||
f"*Trade ID:* `{r['trade_id']}`"
|
"*Trade ID:* `{trade_id}`" + (" `(since {open_date_hum})`" if r["is_open"] else ""),
|
||||||
+ (f" `(since {r['open_date_hum']})`" if r["is_open"] else ""),
|
"*Current Pair:* {pair}",
|
||||||
f"*Current Pair:* {r['pair']}",
|
|
||||||
(
|
(
|
||||||
f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}"
|
f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}"
|
||||||
+ (f" ` ({r['leverage']}x)`" if r.get("leverage") else "")
|
+ " ` ({leverage}x)`"
|
||||||
|
if r.get("leverage")
|
||||||
|
else ""
|
||||||
),
|
),
|
||||||
f"*Amount:* `{r['amount']} ({r['stake_amount_r']})`",
|
"*Amount:* `{amount} ({stake_amount_r})`",
|
||||||
f"*Total invested:* `{r['max_stake_amount_r']}`" if position_adjust else "",
|
"*Total invested:* `{max_stake_amount_r}`" if position_adjust else "",
|
||||||
f"*Enter Tag:* `{r['enter_tag']}`" if r["enter_tag"] else "",
|
"*Enter Tag:* `{enter_tag}`" if r["enter_tag"] else "",
|
||||||
f"*Exit Reason:* `{r['exit_reason']}`" if r["exit_reason"] else "",
|
"*Exit Reason:* `{exit_reason}`" if r["exit_reason"] else "",
|
||||||
]
|
]
|
||||||
|
|
||||||
if position_adjust:
|
if position_adjust:
|
||||||
max_buy_str = f"/{max_entries + 1}" if (max_entries > 0) else ""
|
max_buy_str = f"/{max_entries + 1}" if (max_entries > 0) else ""
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
f"*Number of Entries:* `{r['num_entries']}{max_buy_str}`",
|
"*Number of Entries:* `{num_entries}" + max_buy_str + "`",
|
||||||
f"*Number of Exits:* `{r['num_exits']}`",
|
"*Number of Exits:* `{num_exits}`",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -800,15 +801,15 @@ class Telegram(RPCHandler):
|
|||||||
[
|
[
|
||||||
f"*Open Rate:* `{round_value(r['open_rate'], 8)}`",
|
f"*Open Rate:* `{round_value(r['open_rate'], 8)}`",
|
||||||
f"*Close Rate:* `{round_value(r['close_rate'], 8)}`" if r["close_rate"] else "",
|
f"*Close Rate:* `{round_value(r['close_rate'], 8)}`" if r["close_rate"] else "",
|
||||||
f"*Open Date:* `{r['open_date']}`",
|
"*Open Date:* `{open_date}`",
|
||||||
f"*Close Date:* `{r['close_date']}`" if r["close_date"] else "",
|
"*Close Date:* `{close_date}`" if r["close_date"] else "",
|
||||||
(
|
(
|
||||||
f" \n*Current Rate:* `{round_value(r['current_rate'], 8)}`"
|
f" \n*Current Rate:* `{round_value(r['current_rate'], 8)}`"
|
||||||
if r["is_open"]
|
if r["is_open"]
|
||||||
else ""
|
else ""
|
||||||
),
|
),
|
||||||
("*Unrealized Profit:* " if r["is_open"] else "*Close Profit: *")
|
("*Unrealized Profit:* " if r["is_open"] else "*Close Profit: *")
|
||||||
+ f"`{r['profit_ratio']:.2%}` `({r['profit_abs_r']})`",
|
+ "`{profit_ratio:.2%}` `({profit_abs_r})`",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -816,44 +817,37 @@ class Telegram(RPCHandler):
|
|||||||
if r.get("realized_profit"):
|
if r.get("realized_profit"):
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
f"*Realized Profit:* `{r['realized_profit_ratio']:.2%} "
|
"*Realized Profit:* `{realized_profit_ratio:.2%} "
|
||||||
f"({r['realized_profit_r']})`",
|
"({realized_profit_r})`",
|
||||||
(
|
"*Total Profit:* `{total_profit_ratio:.2%} ({total_profit_abs_r})`",
|
||||||
f"*Total Profit:* `{r['total_profit_ratio']:.2%} "
|
|
||||||
f"({r['total_profit_abs_r']})`",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Append empty line to improve readability
|
# Append empty line to improve readability
|
||||||
lines.append(" ")
|
lines.append(" ")
|
||||||
# Adding liquidation only if it is not None
|
|
||||||
if liquidation := r.get("liquidation_price"):
|
|
||||||
lines.append(f"*Liquidation:* `{round_value(liquidation, 8)}`")
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
r["stop_loss_abs"] != r["initial_stop_loss_abs"]
|
r["stop_loss_abs"] != r["initial_stop_loss_abs"]
|
||||||
and r["initial_stop_loss_ratio"] is not None
|
and r["initial_stop_loss_ratio"] is not None
|
||||||
):
|
):
|
||||||
# Adding initial stoploss only if it is different from stoploss
|
# Adding initial stoploss only if it is different from stoploss
|
||||||
lines.append(
|
lines.append(
|
||||||
f"*Initial Stoploss:* `{r['initial_stop_loss_abs']:.8f}` "
|
"*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
|
||||||
f"`({r['initial_stop_loss_ratio']:.2%})`"
|
"`({initial_stop_loss_ratio:.2%})`"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Adding stoploss and stoploss percentage only if it is not None
|
# Adding stoploss and stoploss percentage only if it is not None
|
||||||
lines.append(
|
lines.append(
|
||||||
f"*Stoploss:* `{round_value(r['stop_loss_abs'], 8)}` "
|
f"*Stoploss:* `{round_value(r['stop_loss_abs'], 8)}` "
|
||||||
+ (f"`({r['stop_loss_ratio']:.2%})`" if r["stop_loss_ratio"] else "")
|
+ ("`({stop_loss_ratio:.2%})`" if r["stop_loss_ratio"] else "")
|
||||||
)
|
)
|
||||||
lines.append(
|
lines.append(
|
||||||
f"*Stoploss distance:* `{round_value(r['stoploss_current_dist'], 8)}` "
|
f"*Stoploss distance:* `{round_value(r['stoploss_current_dist'], 8)}` "
|
||||||
f"`({r['stoploss_current_dist_ratio']:.2%})`"
|
"`({stoploss_current_dist_ratio:.2%})`"
|
||||||
)
|
)
|
||||||
if open_orders := r.get("open_orders"):
|
if r.get("open_orders"):
|
||||||
lines.append(
|
lines.append(
|
||||||
f"*Open Order:* `{open_orders}`"
|
"*Open Order:* `{open_orders}`"
|
||||||
+ (f"- `{r['exit_order_status']}`" if r["exit_order_status"] else "")
|
+ ("- `{exit_order_status}`" if r["exit_order_status"] else "")
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.__send_status_msg(lines, r)
|
await self.__send_status_msg(lines, r)
|
||||||
@@ -869,10 +863,10 @@ class Telegram(RPCHandler):
|
|||||||
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
|
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
|
||||||
msg += line + "\n"
|
msg += line + "\n"
|
||||||
else:
|
else:
|
||||||
await self._send_msg(msg)
|
await self._send_msg(msg.format(**r))
|
||||||
msg = f"*Trade ID:* `{r['trade_id']}` - continued\n" + line + "\n"
|
msg = "*Trade ID:* `{trade_id}` - continued\n" + line + "\n"
|
||||||
|
|
||||||
await self._send_msg(msg)
|
await self._send_msg(msg.format(**r))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
async def _status_table(self, update: Update, context: CallbackContext) -> None:
|
async def _status_table(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
|||||||
@@ -1,23 +1,12 @@
|
|||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.util.migrations.binance_mig import (
|
from freqtrade.util.migrations.binance_mig import migrate_binance_futures_data
|
||||||
migrate_binance_futures_data,
|
|
||||||
migrate_binance_futures_names,
|
|
||||||
)
|
|
||||||
from freqtrade.util.migrations.funding_rate_mig import migrate_funding_fee_timeframe
|
from freqtrade.util.migrations.funding_rate_mig import migrate_funding_fee_timeframe
|
||||||
|
|
||||||
|
|
||||||
def migrate_data(config, exchange: Exchange | None = None) -> None:
|
def migrate_data(config, exchange: Exchange | None = None):
|
||||||
"""
|
"""
|
||||||
Migrate persisted data from old formats to new formats
|
Migrate persisted data from old formats to new formats
|
||||||
"""
|
"""
|
||||||
migrate_binance_futures_data(config)
|
migrate_binance_futures_data(config)
|
||||||
|
|
||||||
migrate_funding_fee_timeframe(config, exchange)
|
migrate_funding_fee_timeframe(config, exchange)
|
||||||
|
|
||||||
|
|
||||||
def migrate_live_content(config, exchange: Exchange | None = None) -> None:
|
|
||||||
"""
|
|
||||||
Migrate database content from old formats to new formats
|
|
||||||
Used for dry/live mode.
|
|
||||||
"""
|
|
||||||
migrate_binance_futures_names(config)
|
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def migrate_binance_futures_names(config: Config):
|
def migrate_binance_futures_names(config: Config):
|
||||||
"""
|
|
||||||
Migrate binance futures names in both database and data files.
|
|
||||||
This is needed because ccxt naming changed from "BTC/USDT" to "BTC/USDT:USDT"
|
|
||||||
"""
|
|
||||||
if not (
|
if not (
|
||||||
config.get("trading_mode", TradingMode.SPOT) == TradingMode.FUTURES
|
config.get("trading_mode", TradingMode.SPOT) == TradingMode.FUTURES
|
||||||
and config["exchange"]["name"] == "binance"
|
and config["exchange"]["name"] == "binance"
|
||||||
|
|||||||
@@ -183,7 +183,6 @@ skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*"
|
|||||||
known_first_party = ["freqtrade_client"]
|
known_first_party = ["freqtrade_client"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
# TODO: should be migrated to [tool.pytest] as support for this was added in 9.0
|
|
||||||
log_format = "%(asctime)s %(levelname)s %(message)s"
|
log_format = "%(asctime)s %(levelname)s %(message)s"
|
||||||
log_date_format = "%Y-%m-%d %H:%M:%S"
|
log_date_format = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
-r requirements-freqai-rl.txt
|
-r requirements-freqai-rl.txt
|
||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
ruff==0.14.4
|
ruff==0.14.3
|
||||||
mypy==1.18.2
|
mypy==1.18.2
|
||||||
pre-commit==4.4.0
|
pre-commit==4.3.0
|
||||||
pytest==9.0.1
|
pytest==8.4.2
|
||||||
pytest-asyncio==1.3.0
|
pytest-asyncio==1.2.0
|
||||||
pytest-cov==7.0.0
|
pytest-cov==7.0.0
|
||||||
pytest-mock==3.15.1
|
pytest-mock==3.15.1
|
||||||
pytest-random-order==1.2.0
|
pytest-random-order==1.2.0
|
||||||
@@ -29,4 +29,4 @@ types-cachetools==6.2.0.20251022
|
|||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.32.4.20250913
|
types-requests==2.32.4.20250913
|
||||||
types-tabulate==0.9.0.20241207
|
types-tabulate==0.9.0.20241207
|
||||||
types-python-dateutil==2.9.0.20251108
|
types-python-dateutil==2.9.0.20251008
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
-r requirements-freqai.txt
|
-r requirements-freqai.txt
|
||||||
|
|
||||||
# Required for freqai-rl
|
# Required for freqai-rl
|
||||||
torch==2.9.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
torch==2.9.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
||||||
gymnasium==1.2.2
|
gymnasium==1.2.2
|
||||||
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
|
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
|
||||||
stable_baselines3==2.7.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
stable_baselines3==2.7.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# Required for freqai
|
# Required for freqai
|
||||||
scikit-learn==1.7.2
|
scikit-learn==1.7.2
|
||||||
joblib==1.5.2
|
joblib==1.5.2
|
||||||
catboost==1.2.8; 'arm' not in platform_machine
|
catboost==1.2.8; 'arm' not in platform_machine and python_version < '3.14'
|
||||||
lightgbm==4.6.0
|
lightgbm==4.6.0
|
||||||
xgboost==3.1.1
|
xgboost==3.1.1
|
||||||
tensorboard==2.20.0
|
tensorboard==2.20.0
|
||||||
|
|||||||
@@ -5,5 +5,5 @@
|
|||||||
scipy==1.16.3
|
scipy==1.16.3
|
||||||
scikit-learn==1.7.2
|
scikit-learn==1.7.2
|
||||||
filelock==3.20.0
|
filelock==3.20.0
|
||||||
optuna==4.6.0
|
optuna==4.5.0
|
||||||
cmaes==0.12.0
|
cmaes==0.12.0
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ ft-pandas-ta==0.3.16
|
|||||||
ta-lib==0.6.8
|
ta-lib==0.6.8
|
||||||
technical==1.5.3
|
technical==1.5.3
|
||||||
|
|
||||||
ccxt==4.5.20
|
ccxt==4.5.17
|
||||||
cryptography==46.0.3
|
cryptography==46.0.3
|
||||||
aiohttp==3.13.2
|
aiohttp==3.13.2
|
||||||
SQLAlchemy==2.0.44
|
SQLAlchemy==2.0.44
|
||||||
@@ -18,7 +18,7 @@ humanize==4.14.0
|
|||||||
cachetools==6.2.1
|
cachetools==6.2.1
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
urllib3==2.5.0
|
urllib3==2.5.0
|
||||||
certifi==2025.11.12
|
certifi==2025.10.5
|
||||||
jsonschema==4.25.1
|
jsonschema==4.25.1
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
pycoingecko==3.2.0
|
pycoingecko==3.2.0
|
||||||
@@ -37,7 +37,7 @@ orjson==3.11.4
|
|||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.121.1
|
fastapi==0.121.0
|
||||||
pydantic==2.12.4
|
pydantic==2.12.4
|
||||||
uvicorn==0.38.0
|
uvicorn==0.38.0
|
||||||
pyjwt==2.10.1
|
pyjwt==2.10.1
|
||||||
|
|||||||
@@ -198,8 +198,6 @@ def test_list_timeframes(mocker, capsys):
|
|||||||
"1h": "hour",
|
"1h": "hour",
|
||||||
"1d": "day",
|
"1d": "day",
|
||||||
}
|
}
|
||||||
api_mock.options = {}
|
|
||||||
|
|
||||||
patch_exchange(mocker, api_mock=api_mock, exchange="bybit")
|
patch_exchange(mocker, api_mock=api_mock, exchange="bybit")
|
||||||
args = [
|
args = [
|
||||||
"list-timeframes",
|
"list-timeframes",
|
||||||
@@ -288,52 +286,6 @@ def test_list_timeframes(mocker, capsys):
|
|||||||
assert re.search(r"^1h$", captured.out, re.MULTILINE)
|
assert re.search(r"^1h$", captured.out, re.MULTILINE)
|
||||||
assert re.search(r"^1d$", captured.out, re.MULTILINE)
|
assert re.search(r"^1d$", captured.out, re.MULTILINE)
|
||||||
|
|
||||||
api_mock.options = {
|
|
||||||
"timeframes": {
|
|
||||||
"spot": {"1m": "1m", "5m": "5m", "15m": "15m"},
|
|
||||||
"swap": {"1m": "1m", "15m": "15m", "1h": "1h"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args = [
|
|
||||||
"list-timeframes",
|
|
||||||
"--exchange",
|
|
||||||
"binance",
|
|
||||||
]
|
|
||||||
start_list_timeframes(get_args(args))
|
|
||||||
captured = capsys.readouterr()
|
|
||||||
assert re.match(
|
|
||||||
"Timeframes available for the exchange `Binance`: 1m, 5m, 15m",
|
|
||||||
captured.out,
|
|
||||||
)
|
|
||||||
|
|
||||||
args = [
|
|
||||||
"list-timeframes",
|
|
||||||
"--exchange",
|
|
||||||
"binance",
|
|
||||||
"--trading-mode",
|
|
||||||
"spot",
|
|
||||||
]
|
|
||||||
start_list_timeframes(get_args(args))
|
|
||||||
captured = capsys.readouterr()
|
|
||||||
assert re.match(
|
|
||||||
"Timeframes available for the exchange `Binance`: 1m, 5m, 15m",
|
|
||||||
captured.out,
|
|
||||||
)
|
|
||||||
args = [
|
|
||||||
"list-timeframes",
|
|
||||||
"--exchange",
|
|
||||||
"binance",
|
|
||||||
"--trading-mode",
|
|
||||||
"futures",
|
|
||||||
]
|
|
||||||
start_list_timeframes(get_args(args))
|
|
||||||
captured = capsys.readouterr()
|
|
||||||
assert re.match(
|
|
||||||
"Timeframes available for the exchange `Binance`: 1m, 15m, 1h",
|
|
||||||
captured.out,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_list_markets(mocker, markets_static, capsys):
|
def test_list_markets(mocker, markets_static, capsys):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
|
|||||||
@@ -303,7 +303,6 @@ def mock_order_usdt_6(is_short: bool):
|
|||||||
"side": entry_side(is_short),
|
"side": entry_side(is_short),
|
||||||
"type": "limit",
|
"type": "limit",
|
||||||
"price": 10.0,
|
"price": 10.0,
|
||||||
"cost": 20.0,
|
|
||||||
"amount": 2.0,
|
"amount": 2.0,
|
||||||
"filled": 2.0,
|
"filled": 2.0,
|
||||||
"remaining": 0.0,
|
"remaining": 0.0,
|
||||||
@@ -318,7 +317,6 @@ def mock_order_usdt_6_exit(is_short: bool):
|
|||||||
"side": exit_side(is_short),
|
"side": exit_side(is_short),
|
||||||
"type": "limit",
|
"type": "limit",
|
||||||
"price": 12.0,
|
"price": 12.0,
|
||||||
"cost": 24.0,
|
|
||||||
"amount": 2.0,
|
"amount": 2.0,
|
||||||
"filled": 0.0,
|
"filled": 0.0,
|
||||||
"remaining": 2.0,
|
"remaining": 2.0,
|
||||||
|
|||||||
@@ -575,18 +575,12 @@ def test_calculate_max_drawdown2():
|
|||||||
# No losing trade ...
|
# No losing trade ...
|
||||||
drawdown = calculate_max_drawdown(df, date_col="open_date", value_col="profit")
|
drawdown = calculate_max_drawdown(df, date_col="open_date", value_col="profit")
|
||||||
assert drawdown.drawdown_abs == 0.0
|
assert drawdown.drawdown_abs == 0.0
|
||||||
assert drawdown.low_value == 0.0
|
|
||||||
assert drawdown.current_high_value >= 0.0
|
|
||||||
assert drawdown.current_drawdown_abs == 0.0
|
|
||||||
|
|
||||||
df1 = DataFrame(zip(values[:5], dates[:5], strict=False), columns=["profit", "open_date"])
|
df1 = DataFrame(zip(values[:5], dates[:5], strict=False), columns=["profit", "open_date"])
|
||||||
df1.loc[:, "profit"] = df1["profit"] * -1
|
df1.loc[:, "profit"] = df1["profit"] * -1
|
||||||
# No winning trade ...
|
# No winning trade ...
|
||||||
drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit")
|
drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit")
|
||||||
assert drawdown.drawdown_abs == 0.055545
|
assert drawdown.drawdown_abs == 0.055545
|
||||||
assert drawdown.high_value == 0.0
|
|
||||||
assert drawdown.current_high_value == 0.0
|
|
||||||
assert drawdown.current_drawdown_abs == 0.055545
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
from copy import deepcopy
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException, RetryableOrderError
|
from freqtrade.exceptions import OperationalException, RetryableOrderError
|
||||||
from freqtrade.exchange.common import API_RETRY_COUNT
|
from freqtrade.exchange.common import API_RETRY_COUNT
|
||||||
from freqtrade.util import dt_now, dt_ts, dt_utc
|
from freqtrade.util import dt_now, dt_ts
|
||||||
from tests.conftest import EXMS, get_patched_exchange
|
from tests.conftest import EXMS, get_patched_exchange
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
@@ -194,43 +193,3 @@ def test__lev_prep_bitget(default_conf, mocker):
|
|||||||
assert api_mock.set_margin_mode.call_count == 0
|
assert api_mock.set_margin_mode.call_count == 0
|
||||||
assert api_mock.set_leverage.call_count == 1
|
assert api_mock.set_leverage.call_count == 1
|
||||||
api_mock.set_leverage.assert_called_with(symbol="BTC/USDC:USDC", leverage=19.99)
|
api_mock.set_leverage.assert_called_with(symbol="BTC/USDC:USDC", leverage=19.99)
|
||||||
|
|
||||||
|
|
||||||
def test_check_delisting_time_bitget(default_conf_usdt, mocker):
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bitget")
|
|
||||||
exchange._config["runmode"] = RunMode.BACKTEST
|
|
||||||
delist_fut_mock = MagicMock(return_value=None)
|
|
||||||
mocker.patch.object(exchange, "_check_delisting_futures", delist_fut_mock)
|
|
||||||
|
|
||||||
# Invalid run mode
|
|
||||||
resp = exchange.check_delisting_time("BTC/USDT")
|
|
||||||
assert resp is None
|
|
||||||
assert delist_fut_mock.call_count == 0
|
|
||||||
|
|
||||||
# Delist spot called
|
|
||||||
exchange._config["runmode"] = RunMode.DRY_RUN
|
|
||||||
resp1 = exchange.check_delisting_time("BTC/USDT")
|
|
||||||
assert resp1 is None
|
|
||||||
assert delist_fut_mock.call_count == 0
|
|
||||||
|
|
||||||
# Delist futures called
|
|
||||||
exchange.trading_mode = TradingMode.FUTURES
|
|
||||||
resp1 = exchange.check_delisting_time("BTC/USDT:USDT")
|
|
||||||
assert resp1 is None
|
|
||||||
assert delist_fut_mock.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test__check_delisting_futures_bitget(default_conf_usdt, mocker, markets):
|
|
||||||
markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"])
|
|
||||||
markets["BTC/USDT:USDT"]["info"]["limitOpenTime"] = "-1"
|
|
||||||
markets["SOL/BUSD:BUSD"]["info"]["limitOpenTime"] = "-1"
|
|
||||||
markets["ADA/USDT:USDT"]["info"]["limitOpenTime"] = "1760745600000" # 2025-10-18
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bitget")
|
|
||||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
|
|
||||||
|
|
||||||
resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD")
|
|
||||||
# No delisting date
|
|
||||||
assert resp_sol is None
|
|
||||||
# Has a delisting date
|
|
||||||
resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT")
|
|
||||||
assert resp_ada == dt_utc(2025, 10, 18)
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
from copy import deepcopy
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.enums import MarginMode, RunMode, TradingMode
|
from freqtrade.enums.marginmode import MarginMode
|
||||||
from freqtrade.util import dt_utc
|
from freqtrade.enums.tradingmode import TradingMode
|
||||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
|
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
@@ -215,43 +214,3 @@ def test_bybit__order_needs_price(
|
|||||||
exchange.unified_account = uta
|
exchange.unified_account = uta
|
||||||
|
|
||||||
assert exchange._order_needs_price(side, order_type) == expected
|
assert exchange._order_needs_price(side, order_type) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_check_delisting_time_bybit(default_conf_usdt, mocker):
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit")
|
|
||||||
exchange._config["runmode"] = RunMode.BACKTEST
|
|
||||||
delist_fut_mock = MagicMock(return_value=None)
|
|
||||||
mocker.patch.object(exchange, "_check_delisting_futures", delist_fut_mock)
|
|
||||||
|
|
||||||
# Invalid run mode
|
|
||||||
resp = exchange.check_delisting_time("BTC/USDT:USDT")
|
|
||||||
assert resp is None
|
|
||||||
assert delist_fut_mock.call_count == 0
|
|
||||||
|
|
||||||
# Delist spot called
|
|
||||||
exchange._config["runmode"] = RunMode.DRY_RUN
|
|
||||||
resp1 = exchange.check_delisting_time("BTC/USDT")
|
|
||||||
assert resp1 is None
|
|
||||||
assert delist_fut_mock.call_count == 0
|
|
||||||
|
|
||||||
# Delist futures called
|
|
||||||
exchange.trading_mode = TradingMode.FUTURES
|
|
||||||
resp1 = exchange.check_delisting_time("BTC/USDT:USDT")
|
|
||||||
assert resp1 is None
|
|
||||||
assert delist_fut_mock.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test__check_delisting_futures_bybit(default_conf_usdt, mocker, markets):
|
|
||||||
markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"])
|
|
||||||
markets["BTC/USDT:USDT"]["info"]["deliveryTime"] = "0"
|
|
||||||
markets["SOL/BUSD:BUSD"]["info"]["deliveryTime"] = "0"
|
|
||||||
markets["ADA/USDT:USDT"]["info"]["deliveryTime"] = "1760745600000" # 2025-10-18
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit")
|
|
||||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
|
|
||||||
|
|
||||||
resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD")
|
|
||||||
# SOL has no delisting date
|
|
||||||
assert resp_sol is None
|
|
||||||
# Actually has a delisting date
|
|
||||||
resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT")
|
|
||||||
assert resp_ada == dt_utc(2025, 10, 18)
|
|
||||||
|
|||||||
@@ -742,11 +742,10 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
|
|||||||
def test_validate_timeframes(default_conf, mocker, timeframe):
|
def test_validate_timeframes(default_conf, mocker, timeframe):
|
||||||
default_conf["timeframe"] = timeframe
|
default_conf["timeframe"] = timeframe
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
id_mock = MagicMock(return_value="test_exchange")
|
id_mock = PropertyMock(return_value="test_exchange")
|
||||||
api_mock.id = id_mock
|
type(api_mock).id = id_mock
|
||||||
api_mock.options = {}
|
timeframes = PropertyMock(return_value={"1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"})
|
||||||
timeframes = {"1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"}
|
type(api_mock).timeframes = timeframes
|
||||||
api_mock.timeframes = timeframes
|
|
||||||
|
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}.reload_markets")
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
@@ -758,11 +757,12 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
|
|||||||
def test_validate_timeframes_failed(default_conf, mocker):
|
def test_validate_timeframes_failed(default_conf, mocker):
|
||||||
default_conf["timeframe"] = "3m"
|
default_conf["timeframe"] = "3m"
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
id_mock = MagicMock(return_value="test_exchange")
|
id_mock = PropertyMock(return_value="test_exchange")
|
||||||
api_mock.id = id_mock
|
type(api_mock).id = id_mock
|
||||||
timeframes = {"15s": "15s", "1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"}
|
timeframes = PropertyMock(
|
||||||
api_mock.timeframes = timeframes
|
return_value={"15s": "15s", "1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"}
|
||||||
api_mock.options = {}
|
)
|
||||||
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}.reload_markets")
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
@@ -1110,116 +1110,6 @@ def test_create_dry_run_order_fees(
|
|||||||
assert order1["fee"]["rate"] == fee
|
assert order1["fee"]["rate"] == fee
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"side,limit,offset,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),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test__dry_is_price_crossed_with_orderbook(
|
|
||||||
default_conf, mocker, order_book_l2_usd, side, limit, offset, expected
|
|
||||||
):
|
|
||||||
# Best bid 25.563
|
|
||||||
# Best ask 25.566
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
|
||||||
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
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
assert result is expected
|
|
||||||
|
|
||||||
|
|
||||||
def test__dry_is_price_crossed_empty_orderbook(default_conf, mocker):
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
|
||||||
empty_book = {"asks": [], "bids": []}
|
|
||||||
assert not exchange._dry_is_price_crossed("LTC/USDT", "buy", 100.0, orderbook=empty_book)
|
|
||||||
|
|
||||||
|
|
||||||
def test__dry_is_price_crossed_fetches_orderbook(default_conf, mocker, order_book_l2_usd):
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
|
||||||
exchange.fetch_l2_order_book = order_book_l2_usd
|
|
||||||
assert exchange._dry_is_price_crossed("LTC/USDT", "buy", 26.0)
|
|
||||||
assert order_book_l2_usd.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test__dry_is_price_crossed_without_orderbook_support(default_conf, mocker):
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
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.fetch_l2_order_book.call_count == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"crossed,immediate,side,amount,expected_status,expected_fee_rate,expected_calls,taker_or_maker",
|
|
||||||
[
|
|
||||||
(True, True, "buy", 2.0, "closed", 0.005, 1, "taker"),
|
|
||||||
(True, False, "sell", 1.5, "closed", 0.005, 1, "maker"),
|
|
||||||
(False, False, "sell", 1.0, "open", None, 0, None),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_check_dry_limit_order_filled_parametrized(
|
|
||||||
default_conf,
|
|
||||||
mocker,
|
|
||||||
crossed,
|
|
||||||
immediate,
|
|
||||||
side,
|
|
||||||
amount,
|
|
||||||
expected_status,
|
|
||||||
expected_fee_rate,
|
|
||||||
expected_calls,
|
|
||||||
taker_or_maker,
|
|
||||||
):
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=crossed)
|
|
||||||
fee_mock = mocker.patch(f"{EXMS}.get_fee", return_value=0.005)
|
|
||||||
|
|
||||||
order = {
|
|
||||||
"symbol": "LTC/USDT",
|
|
||||||
"status": "open",
|
|
||||||
"type": "limit",
|
|
||||||
"side": side,
|
|
||||||
"price": 25.0,
|
|
||||||
"amount": amount,
|
|
||||||
"filled": 0.0,
|
|
||||||
"remaining": amount,
|
|
||||||
"cost": 25.0 * amount,
|
|
||||||
"fee": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
result = exchange.check_dry_limit_order_filled(order, immediate=immediate)
|
|
||||||
|
|
||||||
assert result["status"] == expected_status
|
|
||||||
if crossed:
|
|
||||||
assert result["filled"] == amount
|
|
||||||
assert result["remaining"] == 0.0
|
|
||||||
assert result["fee"]["rate"] == expected_fee_rate
|
|
||||||
fee_mock.assert_called_once_with("LTC/USDT", taker_or_maker=taker_or_maker)
|
|
||||||
else:
|
|
||||||
assert result["filled"] == 0.0
|
|
||||||
assert result["remaining"] == amount
|
|
||||||
assert result["fee"] is None
|
|
||||||
assert fee_mock.call_count == expected_calls
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"side,price,filled,converted",
|
"side,price,filled,converted",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import pytest
|
|||||||
|
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||||
from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
|
from freqtrade.exchange.exchange import timeframe_to_msecs
|
||||||
from freqtrade.util import dt_floor_day, dt_now, dt_ts
|
from freqtrade.util import dt_floor_day, dt_now, dt_ts
|
||||||
from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES
|
from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES
|
||||||
|
|
||||||
@@ -422,23 +422,15 @@ class TestCCXTExchange:
|
|||||||
trades_orig = nvspy.call_args_list[2][0][0]
|
trades_orig = nvspy.call_args_list[2][0][0]
|
||||||
assert len(trades_orig[-1].get("info")) > len(trades_orig[-2].get("info"))
|
assert len(trades_orig[-1].get("info")) > len(trades_orig[-2].get("info"))
|
||||||
|
|
||||||
def _ccxt_get_fee(self, exch: Exchange, pair: str):
|
def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
|
exch, exchangename = exchange
|
||||||
|
pair = EXCHANGES[exchangename]["pair"]
|
||||||
threshold = 0.01
|
threshold = 0.01
|
||||||
assert 0 < exch.get_fee(pair, "limit", "buy") < threshold
|
assert 0 < exch.get_fee(pair, "limit", "buy") < threshold
|
||||||
assert 0 < exch.get_fee(pair, "limit", "sell") < threshold
|
assert 0 < exch.get_fee(pair, "limit", "sell") < threshold
|
||||||
assert 0 < exch.get_fee(pair, "market", "buy") < threshold
|
assert 0 < exch.get_fee(pair, "market", "buy") < threshold
|
||||||
assert 0 < exch.get_fee(pair, "market", "sell") < threshold
|
assert 0 < exch.get_fee(pair, "market", "sell") < threshold
|
||||||
|
|
||||||
def test_ccxt_get_fee_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
|
||||||
exch, exchangename = exchange
|
|
||||||
pair = EXCHANGES[exchangename]["pair"]
|
|
||||||
self._ccxt_get_fee(exch, pair)
|
|
||||||
|
|
||||||
def test_ccxt_get_fee_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
|
||||||
exch, exchangename = exchange_futures
|
|
||||||
pair = EXCHANGES[exchangename].get("futures_pair", EXCHANGES[exchangename]["pair"])
|
|
||||||
self._ccxt_get_fee(exch, pair)
|
|
||||||
|
|
||||||
def test_ccxt_get_max_leverage_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt_get_max_leverage_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
spot, spot_name = exchange
|
spot, spot_name = exchange
|
||||||
if spot:
|
if spot:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@@ -34,6 +35,9 @@ def can_run_model(model: str) -> None:
|
|||||||
if is_arm() and "Catboost" in model:
|
if is_arm() and "Catboost" in model:
|
||||||
pytest.skip("CatBoost is not supported on ARM.")
|
pytest.skip("CatBoost is not supported on ARM.")
|
||||||
|
|
||||||
|
if "Catboost" in model and sys.version_info >= (3, 14):
|
||||||
|
pytest.skip("CatBoost is not supported on Python 3.14+.")
|
||||||
|
|
||||||
if is_pytorch_model and is_mac():
|
if is_pytorch_model and is_mac():
|
||||||
pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.")
|
pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.")
|
||||||
|
|
||||||
|
|||||||
@@ -4525,7 +4525,6 @@ def test_check_for_open_trades(mocker, default_conf_usdt, fee, is_short):
|
|||||||
def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_short):
|
def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_short):
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
create_mock_trades(fee, is_short=is_short)
|
create_mock_trades(fee, is_short=is_short)
|
||||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
|
|
||||||
|
|
||||||
freqtrade.startup_update_open_orders()
|
freqtrade.startup_update_open_orders()
|
||||||
assert not log_has_re(r"Error updating Order .*", caplog)
|
assert not log_has_re(r"Error updating Order .*", caplog)
|
||||||
|
|||||||
Reference in New Issue
Block a user