mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-02-13 01:30:35 +00:00
Merge branch 'develop' into fix/tz-naive-predictions
This commit is contained in:
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -29,6 +29,10 @@ updates:
|
||||
mkdocs:
|
||||
patterns:
|
||||
- "mkdocs*"
|
||||
scipy:
|
||||
patterns:
|
||||
- "scipy"
|
||||
- "scipy-stubs"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
check-latest: true
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -301,7 +301,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -449,7 +449,7 @@ jobs:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
|
||||
2
.github/workflows/devcontainer-build.yml
vendored
2
.github/workflows/devcontainer-build.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -21,18 +21,18 @@ repos:
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.16.1"
|
||||
rev: "v1.17.1"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==6.0.0.20250525
|
||||
- types-cachetools==6.1.0.20250717
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.4.20250611
|
||||
- types-tabulate==0.9.0.20241207
|
||||
- types-python-dateutil==2.9.0.20250708
|
||||
- scipy-stubs==1.16.0.2
|
||||
- SQLAlchemy==2.0.41
|
||||
- scipy-stubs==1.16.1.0
|
||||
- SQLAlchemy==2.0.42
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -44,7 +44,7 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.12.3'
|
||||
rev: 'v0.12.7'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
Binary file not shown.
@@ -180,6 +180,16 @@
|
||||
"description": "Offset for profit exit. \nUsually specified in the strategy and missing in the configuration.",
|
||||
"type": "number"
|
||||
},
|
||||
"recursive_strategy_search": {
|
||||
"description": "Enable recursive strategy search.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"user_data_dir": {
|
||||
"description": "Path to the user data directory."
|
||||
},
|
||||
"datadir": {
|
||||
"description": "Path to the data directory."
|
||||
},
|
||||
"fee": {
|
||||
"description": "Trading fee percentage. Can help to simulate slippage in backtesting",
|
||||
"type": "number",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
FROM freqtradeorg/freqtrade:develop
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements-dev.txt /freqtrade/
|
||||
|
||||
RUN pip install numpy --user --no-cache-dir \
|
||||
&& pip install -r requirements-dev.txt --user --no-cache-dir
|
||||
|
||||
# Empty the ENTRYPOINT to allow all commands
|
||||
ENTRYPOINT []
|
||||
@@ -321,6 +321,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| SQN | 2.45 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Avg. daily profit | 0.0001 BTC |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
| Total trade volume | 0.429 BTC |
|
||||
| | |
|
||||
@@ -374,6 +375,8 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
- `Calmar`: Annualized Calmar ratio.
|
||||
- `SQN`: System Quality Number (SQN) - by Van Tharp.
|
||||
- `Profit factor`: profit / loss.
|
||||
- `Expectancy (Ratio)`: Expectancy ratio, which is the average profit or loss per trade. A negative expectancy ratio means that your strategy is not profitable.
|
||||
- `Avg. daily profit`: Average profit per day, calculated as `(Total Profit / Backtest Days)`.
|
||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||
- `Best Pair` / `Worst Pair`: Best and worst performing pair (based on absolute profit), and it's corresponding `Tot Profit %`.
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
```
|
||||
usage: freqtrade list-exchanges [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[-c PATH] [-d PATH] [--userdir PATH] [-1] [-a]
|
||||
[--trading-mode {spot,margin,futures}]
|
||||
[--dex-exchanges]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-1, --one-column Print output in one column.
|
||||
-a, --all Print all exchanges known to the ccxt library.
|
||||
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
||||
Select Trading mode
|
||||
--dex-exchanges Print only DEX exchanges.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
||||
@@ -328,6 +328,22 @@ It's therefore required to pass the UID as well.
|
||||
!!! Warning "Necessary Verification"
|
||||
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
|
||||
|
||||
## Bitget
|
||||
|
||||
Bitget requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "bitget",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"password": "your_exchange_api_key_password",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Bitget supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||
|
||||
## Hyperliquid
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
@@ -339,13 +355,13 @@ This needs to be configured like this:
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "hyperliquid",
|
||||
"walletAddress": "your_eth_wallet_address",
|
||||
"walletAddress": "your_eth_wallet_address", // This should NOT be your API Wallet Address!
|
||||
"privateKey": "your_api_private_key",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your wallet address, not your API Wallet Address.
|
||||
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your main wallet address, not your API Wallet Address.
|
||||
* privateKey in hex format: `0x<64 hex characters>` - Use the key the API Wallet shows on creation.
|
||||
|
||||
Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed.
|
||||
@@ -363,6 +379,27 @@ Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer
|
||||
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid.
|
||||
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
|
||||
|
||||
### Hyperliquid Vault / Subaccount
|
||||
|
||||
Hyperliquid allows you to create either a vault or a subaccount.
|
||||
To use these with Freqtrade, you will need to use the following configuration pattern:
|
||||
|
||||
``` json
|
||||
"exchange": {
|
||||
"name": "hyperliquid",
|
||||
"walletAddress": "your_vault_address", // Vault or subaccount address
|
||||
"privateKey": "your_api_private_key",
|
||||
"ccxt_config": {
|
||||
"options": {
|
||||
"vaultAddress": "your_vault_address" // Optional, only if you want to use a vault or subaccount
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Your balance and trades will now be used from your vault / subaccount - and no longer from your main account.
|
||||
|
||||
### Historic Hyperliquid data
|
||||
|
||||
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
markdown==3.8.2
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.6.15
|
||||
mkdocs-material==9.6.16
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.16
|
||||
pymdown-extensions==10.16.1
|
||||
jinja2==3.1.6
|
||||
mike==2.1.3
|
||||
|
||||
@@ -117,9 +117,9 @@ Different payloads can be configured for different events. Not all fields are ne
|
||||
|
||||
## Webhook Message types
|
||||
|
||||
### Entry
|
||||
### Entry / Entry fill
|
||||
|
||||
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format.
|
||||
The fields in `webhook.entry` and `webhook.entry_fill` are filled when the bot places a long/short Order to increase a position, or when that order fills respectively. Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
@@ -162,31 +162,9 @@ Possible parameters are:
|
||||
* `current_rate`
|
||||
* `enter_tag`
|
||||
|
||||
### Entry fill
|
||||
### Exit / Exit fill
|
||||
|
||||
The fields in `webhook.entry_fill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `direction`
|
||||
* `leverage`
|
||||
* `open_rate`
|
||||
* `amount`
|
||||
* `open_date`
|
||||
* `stake_amount`
|
||||
* `stake_currency`
|
||||
* `base_currency`
|
||||
* `quote_currency`
|
||||
* `fiat_currency`
|
||||
* `order_type`
|
||||
* `current_rate`
|
||||
* `enter_tag`
|
||||
|
||||
### Exit
|
||||
|
||||
The fields in `webhook.exit` are filled when the bot exits a trade. Parameters are filled using string.format.
|
||||
The fields in `webhook.exit` and `webhook.exit_fill` are filled when the bot places an exit order, or when that exit order fills respectively. Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
@@ -195,34 +173,9 @@ Possible parameters are:
|
||||
* `direction`
|
||||
* `leverage`
|
||||
* `gain`
|
||||
* `limit`
|
||||
* `amount`
|
||||
* `open_rate`
|
||||
* `profit_amount`
|
||||
* `profit_ratio`
|
||||
* `stake_currency`
|
||||
* `base_currency`
|
||||
* `quote_currency`
|
||||
* `fiat_currency`
|
||||
* `exit_reason`
|
||||
* `order_type`
|
||||
* `open_date`
|
||||
* `close_date`
|
||||
|
||||
### Exit fill
|
||||
|
||||
The fields in `webhook.exit_fill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `direction`
|
||||
* `leverage`
|
||||
* `gain`
|
||||
* `close_rate`
|
||||
* `amount`
|
||||
* `open_rate`
|
||||
* `current_rate`
|
||||
* `profit_amount`
|
||||
* `profit_ratio`
|
||||
@@ -230,10 +183,14 @@ Possible parameters are:
|
||||
* `base_currency`
|
||||
* `quote_currency`
|
||||
* `fiat_currency`
|
||||
* `enter_tag`
|
||||
* `exit_reason`
|
||||
* `order_type`
|
||||
* `open_date`
|
||||
* `close_date`
|
||||
* `sub_trade`
|
||||
* `is_final_exit`
|
||||
|
||||
|
||||
### Exit cancel
|
||||
|
||||
@@ -246,7 +203,7 @@ Possible parameters are:
|
||||
* `direction`
|
||||
* `leverage`
|
||||
* `gain`
|
||||
* `limit`
|
||||
* `order_rate`
|
||||
* `amount`
|
||||
* `open_rate`
|
||||
* `current_rate`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2025.7-dev"
|
||||
__version__ = "2025.8-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -96,7 +96,7 @@ ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"]
|
||||
|
||||
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"]
|
||||
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
|
||||
|
||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||
|
||||
@@ -258,24 +258,26 @@ ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs"
|
||||
|
||||
# Command level configs - keep at the bottom of the above definitions
|
||||
NO_CONF_REQURIED = [
|
||||
"backtest-filter",
|
||||
"backtesting-show",
|
||||
"convert-data",
|
||||
"convert-trade-data",
|
||||
"download-data",
|
||||
"list-timeframes",
|
||||
"hyperopt-list",
|
||||
"hyperopt-show",
|
||||
"list-data",
|
||||
"list-freqaimodels",
|
||||
"list-hyperoptloss",
|
||||
"list-markets",
|
||||
"list-pairs",
|
||||
"list-strategies",
|
||||
"list-freqaimodels",
|
||||
"list-hyperoptloss",
|
||||
"list-data",
|
||||
"hyperopt-list",
|
||||
"hyperopt-show",
|
||||
"backtest-filter",
|
||||
"list-timeframes",
|
||||
"plot-dataframe",
|
||||
"plot-profit",
|
||||
"show-trades",
|
||||
"trades-to-ohlcv",
|
||||
"install-ui",
|
||||
"strategy-updater",
|
||||
"trades-to-ohlcv",
|
||||
]
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||
@@ -311,8 +313,6 @@ class Arguments:
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
# Allow no-config for certain commands (like downloading / plotting)
|
||||
if "config" in parsed_arg and parsed_arg.config is None:
|
||||
conf_required = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
|
||||
|
||||
if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None:
|
||||
user_dir = parsed_arg.user_data_dir
|
||||
else:
|
||||
@@ -325,7 +325,9 @@ class Arguments:
|
||||
else:
|
||||
# Else use "config.json".
|
||||
cfgfile = Path.cwd() / DEFAULT_CONFIG
|
||||
if cfgfile.is_file() or not conf_required:
|
||||
conf_optional = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
|
||||
if cfgfile.is_file() or not conf_optional:
|
||||
# Only inject config if the file exists, or if the config is required
|
||||
parsed_arg.config = [DEFAULT_CONFIG]
|
||||
|
||||
return parsed_arg
|
||||
|
||||
@@ -369,6 +369,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help="Print all exchanges known to the ccxt library.",
|
||||
action="store_true",
|
||||
),
|
||||
"dex_exchanges": Arg(
|
||||
"--dex-exchanges",
|
||||
help="Print only DEX exchanges.",
|
||||
action="store_true",
|
||||
),
|
||||
# List pairs / markets
|
||||
"list_pairs_all": Arg(
|
||||
"-a",
|
||||
|
||||
@@ -46,7 +46,18 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
table.add_column("Markets")
|
||||
table.add_column("Reason")
|
||||
|
||||
trading_mode = args.get("trading_mode", None)
|
||||
dex_only = args.get("dex_exchanges", False)
|
||||
|
||||
for exchange in available_exchanges:
|
||||
if trading_mode and not any(
|
||||
a["trading_mode"] == trading_mode for a in exchange["trade_modes"]
|
||||
):
|
||||
# If trading_mode is specified, only show exchanges that support it
|
||||
continue
|
||||
if dex_only and not exchange.get("dex", False):
|
||||
# If dex_only is specified, only show DEX exchanges
|
||||
continue
|
||||
name = Text(exchange["name"])
|
||||
if exchange["supported"]:
|
||||
name.append(" (Supported)", style="italic")
|
||||
|
||||
@@ -157,6 +157,16 @@ CONF_SCHEMA = {
|
||||
"description": f"Offset for profit exit. {__IN_STRATEGY}",
|
||||
"type": "number",
|
||||
},
|
||||
"recursive_strategy_search": {
|
||||
"description": "Enable recursive strategy search.",
|
||||
"type": "boolean",
|
||||
},
|
||||
"user_data_dir": {
|
||||
"description": "Path to the user data directory.",
|
||||
},
|
||||
"datadir": {
|
||||
"description": "Path to the data directory.",
|
||||
},
|
||||
"fee": {
|
||||
"description": "Trading fee percentage. Can help to simulate slippage in backtesting",
|
||||
"type": "number",
|
||||
|
||||
@@ -11,7 +11,7 @@ def get_tick_size_over_time(candles: DataFrame) -> Series:
|
||||
# count the number of significant digits for the open and close prices
|
||||
for col in ["open", "high", "low", "close"]:
|
||||
candles[f"{col}_count"] = (
|
||||
candles[col].round(14).astype(str).str.extract(r"\.(\d*[1-9])")[0].str.len()
|
||||
candles[col].round(14).apply("{:.15f}".format).str.extract(r"\.(\d*[1-9])")[0].str.len()
|
||||
)
|
||||
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
|
||||
axis=1
|
||||
|
||||
@@ -6,6 +6,7 @@ from freqtrade.exchange.exchange import Exchange
|
||||
# isort: on
|
||||
from freqtrade.exchange.binance import Binance
|
||||
from freqtrade.exchange.bingx import Bingx
|
||||
from freqtrade.exchange.bitget import Bitget
|
||||
from freqtrade.exchange.bitmart import Bitmart
|
||||
from freqtrade.exchange.bitpanda import Bitpanda
|
||||
from freqtrade.exchange.bitvavo import Bitvavo
|
||||
@@ -45,4 +46,4 @@ from freqtrade.exchange.kucoin import Kucoin
|
||||
from freqtrade.exchange.lbank import Lbank
|
||||
from freqtrade.exchange.luno import Luno
|
||||
from freqtrade.exchange.modetrade import Modetrade
|
||||
from freqtrade.exchange.okx import Okx
|
||||
from freqtrade.exchange.okx import MyOkx, Okx
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
52
freqtrade/exchange/bitget.py
Normal file
52
freqtrade/exchange/bitget.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
from freqtrade.util.datetime_helpers import dt_now, dt_ts
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Bitget(Exchange):
|
||||
"""
|
||||
Bitget exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
|
||||
Please note that this exchange is not included in the list of exchanges
|
||||
officially supported by the Freqtrade development team. So some features
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
}
|
||||
_ft_has_futures: FtHas = {
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
}
|
||||
|
||||
def ohlcv_candle_limit(
|
||||
self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
|
||||
) -> int:
|
||||
"""
|
||||
Exchange ohlcv candle limit
|
||||
bitget has the following behaviour:
|
||||
* 1000 candles for up-to-date data
|
||||
* 200 candles for historic data (prior to a certain date)
|
||||
:param timeframe: Timeframe to check
|
||||
:param candle_type: Candle-type
|
||||
:param since_ms: Starting timestamp
|
||||
:return: Candle limit as integer
|
||||
"""
|
||||
timeframe_map = self._api.options["fetchOHLCV"]["maxRecentDaysPerTimeframe"]
|
||||
days = timeframe_map.get(timeframe, 30)
|
||||
|
||||
if candle_type in (CandleType.FUTURES, CandleType.SPOT) and (
|
||||
not since_ms or dt_ts(dt_now() - timedelta(days=days)) < since_ms
|
||||
):
|
||||
return 1000
|
||||
|
||||
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
|
||||
@@ -1,8 +1,5 @@
|
||||
"""Bybit exchange subclass"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import ccxt
|
||||
|
||||
@@ -12,6 +9,7 @@ from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalExcep
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -76,14 +74,11 @@ class Bybit(Exchange):
|
||||
config = {}
|
||||
if self.trading_mode == TradingMode.SPOT:
|
||||
config.update({"options": {"defaultType": "spot"}})
|
||||
config.update(super()._ccxt_config)
|
||||
elif self.trading_mode == TradingMode.FUTURES:
|
||||
config.update({"options": {"defaultSettle": self._config["stake_currency"]}})
|
||||
config = deep_merge_dicts(config, super()._ccxt_config)
|
||||
return config
|
||||
|
||||
def market_is_future(self, market: dict[str, Any]) -> bool:
|
||||
main = super().market_is_future(market)
|
||||
# For ByBit, we'll only support USDT markets for now.
|
||||
return main and market["settle"] == "USDT"
|
||||
|
||||
@retrier
|
||||
def additional_exchange_init(self) -> None:
|
||||
"""
|
||||
@@ -182,18 +177,36 @@ class Bybit(Exchange):
|
||||
PERPETUAL:
|
||||
bybit:
|
||||
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
|
||||
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
|
||||
USDT:
|
||||
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
|
||||
USDC:
|
||||
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#c
|
||||
|
||||
Long:
|
||||
Long USDT:
|
||||
Liquidation Price = (
|
||||
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
- (Extra Margin Added/Contract Quantity))
|
||||
Short USDT:
|
||||
Liquidation Price = (
|
||||
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
+ (Extra Margin Added/Contract Quantity))
|
||||
|
||||
Long USDC:
|
||||
Liquidation Price = (
|
||||
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
- (Extra Margin Added/Contract Quantity))
|
||||
Short:
|
||||
Position Entry Price - [
|
||||
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
|
||||
]
|
||||
)
|
||||
|
||||
Short USDC:
|
||||
Liquidation Price = (
|
||||
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
+ (Extra Margin Added/Contract Quantity))
|
||||
Position Entry Price + [
|
||||
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
|
||||
]
|
||||
)
|
||||
|
||||
Implementation Note: Extra margin is currently not used.
|
||||
Due to this - the liquidation formula between USDT and USDC is the same.
|
||||
|
||||
:param pair: Pair to calculate liquidation price for
|
||||
:param open_rate: Entry price of position
|
||||
|
||||
@@ -46,10 +46,9 @@ BAD_EXCHANGES = {
|
||||
|
||||
MAP_EXCHANGE_CHILDCLASS = {
|
||||
"binanceus": "binance",
|
||||
"binanceje": "binance",
|
||||
"binanceusdm": "binance",
|
||||
"okex": "okx",
|
||||
"myokx": "okx",
|
||||
"okxus": "okx",
|
||||
"gateio": "gate",
|
||||
"huboi": "htx",
|
||||
}
|
||||
@@ -64,6 +63,7 @@ SUPPORTED_EXCHANGES = [
|
||||
"hyperliquid",
|
||||
"kraken",
|
||||
"okx",
|
||||
"myokx",
|
||||
]
|
||||
|
||||
# either the main, or replacement methods (array) is required
|
||||
|
||||
@@ -294,10 +294,6 @@ class Exchange:
|
||||
# Initial markets load
|
||||
self.reload_markets(True, load_leverage_tiers=False)
|
||||
self.validate_config(config)
|
||||
self._startup_candle_count: int = config.get("startup_candle_count", 0)
|
||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||
self._startup_candle_count, config.get("timeframe", "")
|
||||
)
|
||||
|
||||
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
||||
self.fill_leverage_tiers()
|
||||
@@ -336,6 +332,12 @@ class Exchange:
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop
|
||||
|
||||
def _set_startup_candle_count(self, config: Config) -> None:
|
||||
self._startup_candle_count: int = config.get("startup_candle_count", 0)
|
||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||
self._startup_candle_count, config.get("timeframe", "")
|
||||
)
|
||||
|
||||
def validate_config(self, config: Config) -> None:
|
||||
# Check if timeframe is available
|
||||
self.validate_timeframes(config.get("timeframe"))
|
||||
@@ -350,6 +352,8 @@ class Exchange:
|
||||
self.validate_orderflow(config["exchange"])
|
||||
self.validate_freqai(config)
|
||||
|
||||
self._set_startup_candle_count(config)
|
||||
|
||||
def _init_ccxt(
|
||||
self, exchange_config: dict[str, Any], sync: bool, ccxt_kwargs: dict[str, Any]
|
||||
) -> ccxt.Exchange:
|
||||
|
||||
@@ -213,9 +213,9 @@ def amount_to_precision(
|
||||
amount = float(
|
||||
decimal_to_precision(
|
||||
amount,
|
||||
rounding_mode=TRUNCATE,
|
||||
precision=precision,
|
||||
counting_mode=precisionMode,
|
||||
TRUNCATE, # rounding_mode
|
||||
precision, # numPrecisionDigits
|
||||
precisionMode, # counting_mode
|
||||
)
|
||||
)
|
||||
|
||||
@@ -311,11 +311,11 @@ def price_to_precision(
|
||||
return float(
|
||||
decimal_to_precision(
|
||||
price,
|
||||
rounding_mode=rounding_mode,
|
||||
precision=int(price_precision)
|
||||
rounding_mode, # rounding mode
|
||||
int(price_precision)
|
||||
if precisionMode != TICK_SIZE
|
||||
else price_precision,
|
||||
counting_mode=precisionMode,
|
||||
else price_precision, # numPrecisionDigits
|
||||
precisionMode, # counting_mode
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -287,3 +287,14 @@ class Okx(Exchange):
|
||||
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
||||
orders.extend(orders_open)
|
||||
return orders
|
||||
|
||||
|
||||
class MyOkx(Okx):
|
||||
"""
|
||||
MyOkx exchange class.
|
||||
Minimal adjustment to disable futures trading for the EU subsidiary of Okx
|
||||
"""
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
(TradingMode.SPOT, MarginMode.NONE),
|
||||
]
|
||||
|
||||
@@ -493,7 +493,7 @@ class FreqaiDataDrawer:
|
||||
|
||||
dk.data["data_path"] = str(dk.data_path)
|
||||
dk.data["model_filename"] = str(dk.model_filename)
|
||||
dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns)
|
||||
dk.data["training_features_list"] = dk.training_features_list
|
||||
dk.data["label_list"] = dk.label_list
|
||||
|
||||
with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp:
|
||||
|
||||
@@ -514,12 +514,7 @@ class IFreqaiModel(ABC):
|
||||
current coin/bot loop
|
||||
"""
|
||||
|
||||
if "training_features_list_raw" in dk.data:
|
||||
feature_list = dk.data["training_features_list_raw"]
|
||||
else:
|
||||
feature_list = dk.data["training_features_list"]
|
||||
|
||||
if dk.training_features_list != feature_list:
|
||||
if dk.training_features_list != dk.data["training_features_list"]:
|
||||
raise OperationalException(
|
||||
"Trying to access pretrained model with `identifier` "
|
||||
"but found different features furnished by current strategy. "
|
||||
|
||||
@@ -93,14 +93,16 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Remove credentials from original exchange config to avoid accidental credential exposure
|
||||
remove_exchange_credentials(config["exchange"], True)
|
||||
|
||||
self.exchange = ExchangeResolver.load_exchange(
|
||||
self.config, exchange_config=exchange_config, load_leverage_tiers=True
|
||||
)
|
||||
|
||||
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
||||
|
||||
# Check config consistency here since strategies can set certain options
|
||||
validate_config_consistency(config)
|
||||
|
||||
self.exchange = ExchangeResolver.load_exchange(
|
||||
self.config, exchange_config=exchange_config, load_leverage_tiers=True
|
||||
)
|
||||
# Re-validate exchange compatibility
|
||||
self.exchange.validate_config(self.config)
|
||||
|
||||
init_db(self.config["db_url"])
|
||||
|
||||
@@ -1214,6 +1216,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"leverage": trade.leverage if trade.leverage else None,
|
||||
"direction": "Short" if trade.is_short else "Long",
|
||||
"limit": open_rate, # Deprecated (?)
|
||||
"order_rate": open_rate,
|
||||
"open_rate": open_rate,
|
||||
"order_type": order_type or "unknown",
|
||||
"stake_amount": stake_amount,
|
||||
@@ -1250,6 +1253,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"leverage": trade.leverage,
|
||||
"direction": "Short" if trade.is_short else "Long",
|
||||
"limit": trade.open_rate,
|
||||
"order_rate": trade.open_rate,
|
||||
"order_type": order_type,
|
||||
"stake_amount": trade.stake_amount,
|
||||
"open_rate": trade.open_rate,
|
||||
@@ -2245,6 +2249,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"direction": "Short" if trade.is_short else "Long",
|
||||
"gain": gain,
|
||||
"limit": profit_rate or 0,
|
||||
"order_rate": profit_rate or 0,
|
||||
"order_type": order_type,
|
||||
"amount": order.safe_amount_after_fee,
|
||||
"open_rate": trade.open_rate,
|
||||
|
||||
@@ -125,6 +125,7 @@ class LookaheadAnalysis(BaseAnalysis):
|
||||
|
||||
backtesting = Backtesting(prepare_data_config, self.exchange)
|
||||
self.exchange = backtesting.exchange
|
||||
self.local_config["candle_type_def"] = prepare_data_config["candle_type_def"]
|
||||
self._fee = backtesting.fee
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ class RecursiveAnalysis(BaseAnalysis):
|
||||
|
||||
backtesting = Backtesting(prepare_data_config, self.exchange)
|
||||
self.exchange = backtesting.exchange
|
||||
self.local_config["candle_type_def"] = prepare_data_config["candle_type_def"]
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
|
||||
varholder.data, varholder.timerange = backtesting.load_bt_data()
|
||||
|
||||
@@ -332,8 +332,11 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
||||
),
|
||||
),
|
||||
(
|
||||
"Avg. daily profit %",
|
||||
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}",
|
||||
"Avg. daily profit",
|
||||
fmt_coin(
|
||||
(strat_results["profit_total_abs"] / strat_results["backtest_days"]),
|
||||
strat_results["stake_currency"],
|
||||
),
|
||||
),
|
||||
(
|
||||
"Avg. stake amount",
|
||||
|
||||
@@ -598,6 +598,8 @@ def generate_strategy_stats(
|
||||
"timerange": config.get("timerange", ""),
|
||||
"enable_protections": config.get("enable_protections", False),
|
||||
"strategy_name": strategy,
|
||||
"freqaimodel": config.get("freqaimodel", None),
|
||||
"freqai_identifier": config.get("freqai", {}).get("identifier", None),
|
||||
# Parameters relevant for backtesting
|
||||
"stoploss": config["stoploss"],
|
||||
"trailing_stop": config.get("trailing_stop", False),
|
||||
|
||||
@@ -54,7 +54,7 @@ class StrategyResolver(IResolver):
|
||||
strategy.ft_load_params_from_file()
|
||||
# Set attributes
|
||||
# Check if we need to override configuration
|
||||
# (Attribute name, default, subkey)
|
||||
# (Attribute name, default, subkey)
|
||||
attributes = [
|
||||
("minimal_roi", {"0": 10.0}),
|
||||
("timeframe", None),
|
||||
|
||||
@@ -173,6 +173,12 @@ class Profit(BaseModel):
|
||||
bot_start_date: str
|
||||
|
||||
|
||||
class ProfitAll(BaseModel):
|
||||
all: Profit
|
||||
long: Profit | None = None
|
||||
short: Profit | None = None
|
||||
|
||||
|
||||
class SellReason(BaseModel):
|
||||
wins: int
|
||||
losses: int
|
||||
|
||||
@@ -43,6 +43,7 @@ from freqtrade.rpc.api_server.api_schemas import (
|
||||
Ping,
|
||||
PlotConfig,
|
||||
Profit,
|
||||
ProfitAll,
|
||||
ResultMsg,
|
||||
ShowConfig,
|
||||
Stats,
|
||||
@@ -89,7 +90,8 @@ logger = logging.getLogger(__name__)
|
||||
# 2.40: Add hyperopt-loss endpoint
|
||||
# 2.41: Add download-data endpoint
|
||||
# 2.42: Add /pair_history endpoint with live data
|
||||
API_VERSION = 2.42
|
||||
# 2.43: Add /profit_all endpoint
|
||||
API_VERSION = 2.43
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
@@ -148,6 +150,24 @@ def profit(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
return rpc._rpc_trade_statistics(config["stake_currency"], config.get("fiat_display_currency"))
|
||||
|
||||
|
||||
@router.get("/profit_all", response_model=ProfitAll, tags=["info"])
|
||||
def profit_all(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
response = {
|
||||
"all": rpc._rpc_trade_statistics(
|
||||
config["stake_currency"], config.get("fiat_display_currency")
|
||||
),
|
||||
}
|
||||
if config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
|
||||
response["long"] = rpc._rpc_trade_statistics(
|
||||
config["stake_currency"], config.get("fiat_display_currency"), direction="long"
|
||||
)
|
||||
response["short"] = rpc._rpc_trade_statistics(
|
||||
config["stake_currency"], config.get("fiat_display_currency"), direction="short"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/stats", response_model=Stats, tags=["info"])
|
||||
def stats(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_stats()
|
||||
|
||||
@@ -56,7 +56,8 @@ class __RPCEntryExitMsgBase(RPCSendMsgBase):
|
||||
quote_currency: str
|
||||
leverage: float | None
|
||||
direction: str
|
||||
limit: float
|
||||
limit: float # Deprecated, use order_rate instead
|
||||
order_rate: float
|
||||
open_rate: float
|
||||
order_type: str
|
||||
stake_amount: float
|
||||
@@ -87,7 +88,6 @@ class RPCExitMsg(__RPCEntryExitMsgBase):
|
||||
exit_reason: str | None
|
||||
close_date: datetime
|
||||
# current_rate: float | None
|
||||
order_rate: float | None
|
||||
final_profit_ratio: float | None
|
||||
is_final_exit: bool
|
||||
|
||||
|
||||
@@ -39,6 +39,17 @@ class StrategyUpdater:
|
||||
"sell": "exit",
|
||||
}
|
||||
|
||||
# Update function names.
|
||||
# example: `np.NaN` was removed in the NumPy 2.0 release. Use `np.nan` instead.
|
||||
module_replacements = {
|
||||
"numpy": {
|
||||
"aliases": set(),
|
||||
"replacements": [
|
||||
("NaN", "nan"),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
# create a dictionary that maps the old column names to the new ones
|
||||
rename_dict = {"buy": "enter_long", "sell": "exit_long", "buy_tag": "enter_tag"}
|
||||
|
||||
@@ -153,16 +164,24 @@ class NameUpdater(ast_comments.NodeTransformer):
|
||||
def visit_Name(self, node):
|
||||
# if the name is in the mapping, update it
|
||||
node.id = self.check_dict(StrategyUpdater.name_mapping, node.id)
|
||||
|
||||
for mod, info in StrategyUpdater.module_replacements.items():
|
||||
for old_attr, new_attr in info["replacements"]:
|
||||
if node.id == old_attr:
|
||||
node.id = new_attr
|
||||
return node
|
||||
|
||||
def visit_Import(self, node):
|
||||
# do not update the names in import statements
|
||||
for alias in node.names:
|
||||
if alias.name in StrategyUpdater.module_replacements:
|
||||
as_name = alias.asname or alias.name
|
||||
StrategyUpdater.module_replacements[alias.name]["aliases"].add(as_name)
|
||||
return node
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
# if hasattr(node, "module"):
|
||||
# if node.module == "freqtrade.strategy.hyper":
|
||||
# node.module = "freqtrade.strategy"
|
||||
if node.module in StrategyUpdater.module_replacements:
|
||||
mod = node.module
|
||||
StrategyUpdater.module_replacements[node.module]["aliases"].add(mod)
|
||||
return node
|
||||
|
||||
def visit_If(self, node: ast_comments.If):
|
||||
@@ -182,6 +201,12 @@ class NameUpdater(ast_comments.NodeTransformer):
|
||||
and node.attr == "nr_of_successful_buys"
|
||||
):
|
||||
node.attr = "nr_of_successful_entries"
|
||||
if isinstance(node.value, ast_comments.Name):
|
||||
for mod, info in StrategyUpdater.module_replacements.items():
|
||||
if node.value.id in info["aliases"]:
|
||||
for old_attr, new_attr in info["replacements"]:
|
||||
if node.attr == old_attr:
|
||||
node.attr = new_attr
|
||||
return node
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
|
||||
@@ -9,4 +9,4 @@ def get_dry_run_wallet(config: Config) -> int | float:
|
||||
if isinstance(_start_cap := config["dry_run_wallet"], float | int):
|
||||
return _start_cap
|
||||
else:
|
||||
return _start_cap.get("stake_currency")
|
||||
return _start_cap.get(config["stake_currency"], 0.0)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from freqtrade_client.ft_rest_client import FtRestClient
|
||||
|
||||
|
||||
__version__ = "2025.7-dev"
|
||||
__version__ = "2025.8-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
-r requirements-freqai-rl.txt
|
||||
-r docs/requirements-docs.txt
|
||||
|
||||
ruff==0.12.3
|
||||
mypy==1.16.1
|
||||
ruff==0.12.7
|
||||
mypy==1.17.1
|
||||
pre-commit==4.2.0
|
||||
pytest==8.4.1
|
||||
pytest-asyncio==1.0.0
|
||||
pytest-asyncio==1.1.0
|
||||
pytest-cov==6.2.1
|
||||
pytest-mock==3.14.1
|
||||
pytest-random-order==1.2.0
|
||||
@@ -24,8 +24,8 @@ time-machine==2.16.0
|
||||
nbconvert==7.16.6
|
||||
|
||||
# mypy types
|
||||
scipy-stubs==1.16.0.2 # keep in sync with `scipy` in `requirements-hyperopt.txt`
|
||||
types-cachetools==6.0.0.20250525
|
||||
scipy-stubs==1.16.1.0 # keep in sync with `scipy` in `requirements-hyperopt.txt`
|
||||
types-cachetools==6.1.0.20250717
|
||||
types-filelock==3.2.7
|
||||
types-requests==2.32.4.20250611
|
||||
types-tabulate==0.9.0.20241207
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
torch==2.7.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
||||
gymnasium==0.29.1
|
||||
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
|
||||
stable_baselines3==2.6.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
||||
stable_baselines3==2.7.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
||||
sb3_contrib>=2.2.1
|
||||
# Progress bar for stable-baselines3 and sb3-contrib
|
||||
tqdm==4.67.1
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
-r requirements-plot.txt
|
||||
|
||||
# Required for freqai
|
||||
scikit-learn==1.7.0
|
||||
scikit-learn==1.7.1
|
||||
joblib==1.5.1
|
||||
catboost==1.2.8; 'arm' not in platform_machine
|
||||
lightgbm==4.6.0
|
||||
xgboost==3.0.2
|
||||
tensorboard==2.19.0
|
||||
xgboost==3.0.3
|
||||
tensorboard==2.20.0
|
||||
datasieve==0.1.9
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
-r requirements.txt
|
||||
|
||||
# Required for hyperopt
|
||||
scipy==1.16.0
|
||||
scikit-learn==1.7.0
|
||||
scipy==1.16.1
|
||||
scikit-learn==1.7.1
|
||||
filelock==3.18.0
|
||||
optuna==4.4.0
|
||||
cmaes==0.11.1
|
||||
cmaes==0.12.0
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
numpy==2.3.1
|
||||
numpy==2.3.2
|
||||
pandas==2.3.1
|
||||
bottleneck==1.5.0
|
||||
numexpr==2.11.0
|
||||
# Indicator libraries
|
||||
ft-pandas-ta==0.3.15
|
||||
ta-lib==0.5.5
|
||||
technical==1.5.1
|
||||
technical==1.5.2
|
||||
|
||||
ccxt==4.4.94
|
||||
ccxt==4.4.99
|
||||
cryptography==45.0.5
|
||||
aiohttp==3.12.14
|
||||
SQLAlchemy==2.0.41
|
||||
python-telegram-bot==22.2
|
||||
aiohttp==3.12.15
|
||||
SQLAlchemy==2.0.42
|
||||
python-telegram-bot==22.3
|
||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||
httpx>=0.24.1
|
||||
humanize==4.12.3
|
||||
cachetools==6.1.0
|
||||
requests==2.32.4
|
||||
urllib3==2.5.0
|
||||
certifi==2025.7.14
|
||||
jsonschema==4.24.0
|
||||
certifi==2025.8.3
|
||||
jsonschema==4.25.0
|
||||
tabulate==0.9.0
|
||||
pycoingecko==3.2.0
|
||||
jinja2==3.1.6
|
||||
joblib==1.5.1
|
||||
rich==14.0.0
|
||||
pyarrow==20.0.0; platform_machine != 'armv7l'
|
||||
rich==14.1.0
|
||||
pyarrow==21.0.0; platform_machine != 'armv7l'
|
||||
|
||||
# Load ticker files 30% faster
|
||||
python-rapidjson==1.21
|
||||
# Properly format api responses
|
||||
orjson==3.10.18
|
||||
orjson==3.11.1
|
||||
|
||||
# Notify systemd
|
||||
sdnotify==0.3.2
|
||||
|
||||
@@ -133,6 +133,8 @@ def test_list_exchanges(capsys):
|
||||
captured = capsys.readouterr()
|
||||
assert re.search(r"^binance$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^bybit$", captured.out, re.MULTILINE)
|
||||
# An exchange not supporting futures
|
||||
assert re.search(r"^kraken$", captured.out, re.MULTILINE)
|
||||
|
||||
# Test with --all
|
||||
args = [
|
||||
@@ -160,6 +162,32 @@ def test_list_exchanges(capsys):
|
||||
assert re.search(r"^bingx$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^bitmex$", captured.out, re.MULTILINE)
|
||||
|
||||
# Only dex
|
||||
args = [
|
||||
"list-exchanges",
|
||||
"--dex",
|
||||
]
|
||||
|
||||
start_list_exchanges(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert re.search(r"Exchanges available for Freqtrade.*", captured.out)
|
||||
assert not re.search(r".*binance.*", captured.out)
|
||||
assert not re.search(r".*bingx.*", captured.out)
|
||||
assert re.search(r".*hyperliquid.*", captured.out)
|
||||
|
||||
# Only futures
|
||||
args = [
|
||||
"list-exchanges",
|
||||
"--trading-mode",
|
||||
"futures",
|
||||
]
|
||||
|
||||
start_list_exchanges(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert re.search(r"Exchanges available for Freqtrade.*", captured.out)
|
||||
assert re.search(r".*binance.*", captured.out)
|
||||
assert not re.search(r".*kraken.*", captured.out)
|
||||
|
||||
|
||||
def test_list_timeframes(mocker, capsys):
|
||||
api_mock = MagicMock()
|
||||
|
||||
@@ -90,3 +90,94 @@ def test_get_tick_size_over_time_real_data(testdatadir):
|
||||
|
||||
assert all(result <= 0.0001)
|
||||
assert all(result >= 0.00000001)
|
||||
|
||||
|
||||
def test_get_tick_size_over_time_small_numbers():
|
||||
"""
|
||||
Test the get_tick_size_over_time function with predefined data
|
||||
"""
|
||||
# Create test dataframe with different levels of precision
|
||||
data = {
|
||||
"date": [
|
||||
Timestamp("2020-01-01 00:00:00", tz=UTC),
|
||||
Timestamp("2020-01-02 00:00:00", tz=UTC),
|
||||
Timestamp("2020-01-03 00:00:00", tz=UTC),
|
||||
Timestamp("2020-01-15 00:00:00", tz=UTC),
|
||||
Timestamp("2020-01-16 00:00:00", tz=UTC),
|
||||
Timestamp("2020-01-31 00:00:00", tz=UTC),
|
||||
Timestamp("2020-02-01 00:00:00", tz=UTC),
|
||||
Timestamp("2020-02-15 00:00:00", tz=UTC),
|
||||
Timestamp("2020-03-15 00:00:00", tz=UTC),
|
||||
],
|
||||
"open": [
|
||||
0.000000123456,
|
||||
0.0000001234,
|
||||
0.000000123,
|
||||
0.00000012,
|
||||
0.000000123456,
|
||||
0.0000001234,
|
||||
0.00000023456,
|
||||
0.000000234,
|
||||
0.000000234,
|
||||
],
|
||||
"high": [
|
||||
0.000000123457,
|
||||
0.0000001235,
|
||||
0.000000124,
|
||||
0.00000013,
|
||||
0.000000123456,
|
||||
0.0000001235,
|
||||
0.00000023457,
|
||||
0.000000234,
|
||||
0.000000234,
|
||||
],
|
||||
"low": [
|
||||
0.000000123455,
|
||||
0.0000001233,
|
||||
0.000000122,
|
||||
0.00000011,
|
||||
0.000000123456,
|
||||
0.0000001233,
|
||||
0.00000023455,
|
||||
0.000000234,
|
||||
0.000000234,
|
||||
],
|
||||
"close": [
|
||||
0.000000123456,
|
||||
0.0000001234,
|
||||
0.000000123,
|
||||
0.00000012,
|
||||
0.000000123456,
|
||||
0.0000001234,
|
||||
0.00000023456,
|
||||
0.000000234,
|
||||
0.000000234,
|
||||
],
|
||||
"volume": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
}
|
||||
|
||||
candles = DataFrame(data)
|
||||
|
||||
# Calculate significant digits
|
||||
result = get_tick_size_over_time(candles)
|
||||
|
||||
# Check that the result is a pandas Series
|
||||
assert isinstance(result, pd.Series)
|
||||
|
||||
# Check that we have three months of data (Jan, Feb and March 2020 )
|
||||
assert len(result) == 3
|
||||
|
||||
# Before
|
||||
assert result.asof("2019-01-01 00:00:00+00:00") is nan
|
||||
# January should have 5 significant digits (based on 1.23456789 being the most precise value)
|
||||
# which should be converted to 0.00001
|
||||
|
||||
assert result.asof("2020-01-01 00:00:00+00:00") == 0.000000000001
|
||||
assert result.asof("2020-01-01 00:00:00+00:00") == 0.000000000001
|
||||
assert result.asof("2020-02-25 00:00:00+00:00") == 0.00000000001
|
||||
assert result.asof("2020-03-25 00:00:00+00:00") == 0.000000001
|
||||
assert result.asof("2020-04-01 00:00:00+00:00") == 0.000000001
|
||||
# Value far past the last date should be the last value
|
||||
assert result.asof("2025-04-01 00:00:00+00:00") == 0.000000001
|
||||
|
||||
assert result.iloc[0] == 0.000000000001
|
||||
|
||||
@@ -2506,6 +2506,8 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach
|
||||
time_machine.move_to(start + timedelta(hours=99, minutes=30))
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange._set_startup_candle_count(default_conf)
|
||||
|
||||
mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100)
|
||||
assert exchange._startup_candle_count == 0
|
||||
|
||||
@@ -4954,7 +4956,7 @@ def test_validate_trading_mode_and_margin_mode(
|
||||
("binance", "margin", {"options": {"defaultType": "margin"}}),
|
||||
("binance", "futures", {"options": {"defaultType": "swap"}}),
|
||||
("bybit", "spot", {"options": {"defaultType": "spot"}}),
|
||||
("bybit", "futures", {"options": {"defaultType": "swap"}}),
|
||||
("bybit", "futures", {"options": {"defaultType": "swap", "defaultSettle": "USDT"}}),
|
||||
("gate", "futures", {"options": {"defaultType": "swap"}}),
|
||||
("hitbtc", "futures", {"options": {"defaultType": "swap"}}),
|
||||
("kraken", "futures", {"options": {"defaultType": "swap"}}),
|
||||
@@ -4962,10 +4964,10 @@ def test_validate_trading_mode_and_margin_mode(
|
||||
("okx", "futures", {"options": {"defaultType": "swap"}}),
|
||||
],
|
||||
)
|
||||
def test__ccxt_config(default_conf, mocker, exchange_name, trading_mode, ccxt_config):
|
||||
default_conf["trading_mode"] = trading_mode
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
|
||||
def test__ccxt_config(default_conf_usdt, mocker, exchange_name, trading_mode, ccxt_config):
|
||||
default_conf_usdt["trading_mode"] = trading_mode
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name)
|
||||
assert exchange._ccxt_config == ccxt_config
|
||||
|
||||
|
||||
|
||||
@@ -408,13 +408,21 @@ EXCHANGES = {
|
||||
"candle_count": 200,
|
||||
"orderbook_max_entries": 50,
|
||||
},
|
||||
"htx": {
|
||||
"pair": "ETH/BTC",
|
||||
"stake_currency": "BTC",
|
||||
"bitget": {
|
||||
"pair": "BTC/USDT",
|
||||
"stake_currency": "USDT",
|
||||
"hasQuoteVolume": True,
|
||||
"timeframe": "1h",
|
||||
"candle_count": 1000,
|
||||
},
|
||||
# TODO: re-enable htx once certificates work again
|
||||
# "htx": {
|
||||
# "pair": "ETH/BTC",
|
||||
# "stake_currency": "BTC",
|
||||
# "hasQuoteVolume": True,
|
||||
# "timeframe": "1h",
|
||||
# "candle_count": 1000,
|
||||
# },
|
||||
"bitvavo": {
|
||||
"pair": "BTC/EUR",
|
||||
"stake_currency": "EUR",
|
||||
@@ -490,7 +498,7 @@ EXCHANGES = {
|
||||
"pair": "UBTC/USDC",
|
||||
"stake_currency": "USDC",
|
||||
"hasQuoteVolume": False,
|
||||
"timeframe": "1h",
|
||||
"timeframe": "30m",
|
||||
"futures": True,
|
||||
"candle_count": 5000,
|
||||
"orderbook_max_entries": 20,
|
||||
@@ -498,6 +506,8 @@ EXCHANGES = {
|
||||
"hasQuoteVolumeFutures": True,
|
||||
"leverage_tiers_public": False,
|
||||
"leverage_in_spot_market": False,
|
||||
# TODO: re-enable hyperliquid websocket tests
|
||||
"skip_ws_tests": True,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -568,12 +578,16 @@ def get_futures_exchange(exchange_name, exchange_conf, class_mocker):
|
||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||
def exchange(request, exchange_conf, class_mocker):
|
||||
class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init")
|
||||
return get_exchange(request.param, exchange_conf)
|
||||
exchange, name = get_exchange(request.param, exchange_conf)
|
||||
yield exchange, name
|
||||
exchange.close()
|
||||
|
||||
|
||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||
def exchange_futures(request, exchange_conf, class_mocker):
|
||||
return get_futures_exchange(request.param, exchange_conf, class_mocker)
|
||||
exchange, name = get_futures_exchange(request.param, exchange_conf, class_mocker)
|
||||
yield exchange, name
|
||||
exchange.close()
|
||||
|
||||
|
||||
@pytest.fixture(params=["spot", "futures"], scope="class")
|
||||
|
||||
@@ -270,9 +270,7 @@ class TestCCXTExchange:
|
||||
assert exch.klines(pair_tf).iloc[-1]["date"] >= timeframe_to_prev_date(timeframe, now)
|
||||
assert exch.klines(pair_tf)["date"].astype(int).iloc[0] // 1e6 == since_ms
|
||||
|
||||
def ccxt__async_get_candle_history(
|
||||
self, exchange, exchangename, pair, timeframe, candle_type, factor=0.9
|
||||
):
|
||||
def _ccxt__async_get_candle_history(self, exchange, pair, timeframe, candle_type, factor=0.9):
|
||||
timeframe_ms = timeframe_to_msecs(timeframe)
|
||||
now = timeframe_to_prev_date(timeframe, datetime.now(UTC))
|
||||
for offset in (360, 120, 30, 10, 5, 2):
|
||||
@@ -304,7 +302,7 @@ class TestCCXTExchange:
|
||||
pytest.skip("Exchange does not support candle history")
|
||||
pair = EXCHANGES[exchangename]["pair"]
|
||||
timeframe = EXCHANGES[exchangename]["timeframe"]
|
||||
self.ccxt__async_get_candle_history(exc, exchangename, pair, timeframe, CandleType.SPOT)
|
||||
self._ccxt__async_get_candle_history(exc, pair, timeframe, CandleType.SPOT)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"candle_type",
|
||||
@@ -315,7 +313,7 @@ class TestCCXTExchange:
|
||||
],
|
||||
)
|
||||
def test_ccxt__async_get_candle_history_futures(
|
||||
self, exchange_futures: EXCHANGE_FIXTURE_TYPE, candle_type
|
||||
self, exchange_futures: EXCHANGE_FIXTURE_TYPE, candle_type: CandleType
|
||||
):
|
||||
exchange, exchangename = exchange_futures
|
||||
pair = EXCHANGES[exchangename].get("futures_pair", EXCHANGES[exchangename]["pair"])
|
||||
@@ -324,9 +322,8 @@ class TestCCXTExchange:
|
||||
timeframe = exchange._ft_has.get(
|
||||
"funding_fee_timeframe", exchange._ft_has["mark_ohlcv_timeframe"]
|
||||
)
|
||||
self.ccxt__async_get_candle_history(
|
||||
self._ccxt__async_get_candle_history(
|
||||
exchange,
|
||||
exchangename,
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
candle_type=candle_type,
|
||||
@@ -383,6 +380,10 @@ class TestCCXTExchange:
|
||||
this_hour = timeframe_to_prev_date(expected_tf)
|
||||
prev_hour = timeframe_to_prev_date(expected_tf, this_hour - timedelta(minutes=1))
|
||||
|
||||
# Mark price must be available for the currently open candle (as well as older candles,
|
||||
# even though the test only asserts the last two).
|
||||
# This is a requirement to have funding fee calculations available correctly and timely
|
||||
# right as the funding fee applies (e.g. at 08:00).
|
||||
assert mark_candles[mark_candles["date"] == prev_hour].iloc[0]["open"] != 0.0
|
||||
assert mark_candles[mark_candles["date"] == this_hour].iloc[0]["open"] != 0.0
|
||||
|
||||
@@ -396,14 +397,15 @@ class TestCCXTExchange:
|
||||
)
|
||||
|
||||
assert isinstance(funding_fee, float)
|
||||
# assert funding_fee > 0
|
||||
assert funding_fee != 0
|
||||
|
||||
def test_ccxt__async_get_trade_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
def test_ccxt__async_get_trade_history(self, exchange: EXCHANGE_FIXTURE_TYPE, mocker):
|
||||
exch, exchangename = exchange
|
||||
if not (lookback := EXCHANGES[exchangename].get("trades_lookback_hours")):
|
||||
pytest.skip("test_fetch_trades not enabled for this exchange")
|
||||
pair = EXCHANGES[exchangename]["pair"]
|
||||
since = int((datetime.now(UTC) - timedelta(hours=lookback)).timestamp() * 1000)
|
||||
nvspy = mocker.spy(exch, "_get_trade_pagination_next_value")
|
||||
res = exch.loop.run_until_complete(exch._async_get_trade_history(pair, since, None, None))
|
||||
assert len(res) == 2
|
||||
res_pair, res_trades = res
|
||||
@@ -411,6 +413,14 @@ class TestCCXTExchange:
|
||||
assert isinstance(res_trades, list)
|
||||
assert res_trades[0][0] >= since
|
||||
assert len(res_trades) > 1200
|
||||
assert nvspy.call_count > 5
|
||||
if exchangename == "kraken":
|
||||
# for Kraken, the pagination value is added to the last trade result by ccxt.
|
||||
# We therefore expect that the last row has one additional field
|
||||
|
||||
# Pick a random spy call
|
||||
trades_orig = nvspy.call_args_list[2][0][0]
|
||||
assert len(trades_orig[-1].get("info")) > len(trades_orig[-2].get("info"))
|
||||
|
||||
def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
@@ -520,3 +530,35 @@ class TestCCXTExchange:
|
||||
exch, exchangename = exchange
|
||||
for method in EXCHANGES[exchangename].get("private_methods", []):
|
||||
assert hasattr(exch._api, method)
|
||||
|
||||
def test_ccxt_bitget_ohlcv_candle_limit(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
if exchangename != "bitget":
|
||||
pytest.skip("This test is only for the Bitget exchange")
|
||||
|
||||
timeframes = ("1m", "5m", "1h")
|
||||
|
||||
for timeframe in timeframes:
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT) == 1000
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES) == 1000
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK) == 200
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE) == 200
|
||||
|
||||
start_time = dt_ts(dt_now() - timedelta(days=17))
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == 1000
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == 1000
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 200
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
|
||||
start_time = dt_ts(dt_now() - timedelta(days=48))
|
||||
length = 200 if timeframe in ("1m", "5m") else 1000
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == length
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == length
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 200
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
|
||||
|
||||
start_time = dt_ts(dt_now() - timedelta(days=61))
|
||||
length = 200
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == length
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == length
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 200
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
|
||||
|
||||
@@ -35,21 +35,22 @@ class TestCCXTExchangeWs:
|
||||
break
|
||||
sleep(1)
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
res = exch.refresh_latest_ohlcv([pair_tf])
|
||||
assert m_cand.call_count == 1
|
||||
|
||||
# Currently open candle
|
||||
next_candle = timeframe_to_prev_date(timeframe, dt_now())
|
||||
now = next_candle - timedelta(seconds=1)
|
||||
# Currently closed candle
|
||||
curr_candle = timeframe_to_prev_date(timeframe, now)
|
||||
curr_candle = timeframe_to_prev_date(timeframe, next_candle - timedelta(seconds=1))
|
||||
|
||||
assert pair_tf in exch._exchange_ws._klines_watching
|
||||
assert pair_tf in exch._exchange_ws._klines_scheduled
|
||||
assert res[pair_tf] is not None
|
||||
df1 = res[pair_tf]
|
||||
caplog.set_level(logging.DEBUG)
|
||||
assert df1.iloc[-1]["date"] == curr_candle
|
||||
assert df1.iloc[-1]["date"] == curr_candle, (
|
||||
f"Expected {curr_candle}, got {df1.iloc[-1]['date']} for {pair_tf}, now: {dt_now()}"
|
||||
)
|
||||
|
||||
# Wait until the next candle (might be up to 1 minute).
|
||||
while True:
|
||||
|
||||
@@ -1343,6 +1343,44 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
|
||||
}
|
||||
|
||||
|
||||
def test_api_profit_all(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
ftbot.config["tradable_balance_ratio"] = 1
|
||||
ftbot.config["trading_mode"] = TradingMode.FUTURES
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/profit_all")
|
||||
assert_response(rc, 200)
|
||||
response = rc.json()
|
||||
assert "all" in response
|
||||
assert "long" in response
|
||||
assert "short" in response
|
||||
|
||||
assert response["all"]["trade_count"] == 0
|
||||
create_mock_trades_usdt(fee, is_short=None)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/profit_all")
|
||||
assert_response(rc, 200)
|
||||
response = rc.json()
|
||||
assert response["all"]["trade_count"] == 7
|
||||
assert response["long"]["trade_count"] == 2
|
||||
assert response["short"]["trade_count"] == 5
|
||||
assert pytest.approx(response["all"]["profit_all_coin"]) == 22.58997755
|
||||
assert pytest.approx(response["long"]["profit_all_coin"]) == -20.0498903
|
||||
assert pytest.approx(response["short"]["profit_all_coin"]) == 42.639867
|
||||
|
||||
assert response["all"]["best_pair"] == "NEO/USDT"
|
||||
assert response["long"]["best_pair"] == ""
|
||||
assert response["short"]["best_pair"] == "NEO/USDT"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [True, False])
|
||||
def test_api_stats(botclient, mocker, ticker, fee, markets, is_short):
|
||||
ftbot, client = botclient
|
||||
|
||||
@@ -42,8 +42,10 @@ def test_strategy_updater_methods(default_conf, caplog) -> None:
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
modified_code1 = instance_strategy_updater.update_code(
|
||||
"""
|
||||
import numpy as np
|
||||
class testClass(IStrategy):
|
||||
def populate_buy_trend():
|
||||
some_variable = np.NaN
|
||||
pass
|
||||
def populate_sell_trend():
|
||||
pass
|
||||
@@ -62,6 +64,7 @@ class testClass(IStrategy):
|
||||
assert "check_exit_timeout" in modified_code1
|
||||
assert "custom_exit" in modified_code1
|
||||
assert "INTERFACE_VERSION = 3" in modified_code1
|
||||
assert "np.nan" in modified_code1
|
||||
|
||||
|
||||
def test_strategy_updater_params(default_conf, caplog) -> None:
|
||||
|
||||
19
tests/util/test_wallet_util.py
Normal file
19
tests/util/test_wallet_util.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import pytest
|
||||
|
||||
from freqtrade.util import get_dry_run_wallet
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"wallet,stake_currency,expected",
|
||||
[
|
||||
(1000, "USDT", 1000),
|
||||
({"USDT": 1000, "USDC": 500}, "USDT", 1000),
|
||||
({"USDT": 1000, "USDC": 500}, "USDC", 500),
|
||||
({"USDT": 1000, "USDC": 500}, "NOCURR", 0.0),
|
||||
],
|
||||
)
|
||||
def test_get_dry_run_wallet(default_conf_usdt, wallet, stake_currency, expected):
|
||||
# As int
|
||||
default_conf_usdt["dry_run_wallet"] = wallet
|
||||
default_conf_usdt["stake_currency"] = stake_currency
|
||||
assert get_dry_run_wallet(default_conf_usdt) == expected
|
||||
Reference in New Issue
Block a user