Merge branch 'freqtrade:develop' into main-stash
This commit is contained in:
@@ -2,7 +2,7 @@ version: 2
|
|||||||
updates:
|
updates:
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 4
|
default-days: 7
|
||||||
directories:
|
directories:
|
||||||
- "/"
|
- "/"
|
||||||
- "/docker"
|
- "/docker"
|
||||||
@@ -16,7 +16,7 @@ updates:
|
|||||||
- package-ecosystem: devcontainers
|
- package-ecosystem: devcontainers
|
||||||
directory: "/"
|
directory: "/"
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 4
|
default-days: 7
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
@@ -24,7 +24,7 @@ updates:
|
|||||||
- package-ecosystem: pip
|
- package-ecosystem: pip
|
||||||
directory: "/"
|
directory: "/"
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 4
|
default-days: 7
|
||||||
exclude:
|
exclude:
|
||||||
- ccxt
|
- ccxt
|
||||||
schedule:
|
schedule:
|
||||||
@@ -51,7 +51,7 @@ updates:
|
|||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 4
|
default-days: 7
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
environment:
|
environment:
|
||||||
name: develop
|
name: develop
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
run: python build_helpers/binance_update_lev_tiers.py
|
run: python build_helpers/binance_update_lev_tiers.py
|
||||||
|
|
||||||
|
|
||||||
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||||
add-paths: freqtrade/exchange/binance_leverage_tiers.json
|
add-paths: freqtrade/exchange/binance_leverage_tiers.json
|
||||||
|
|||||||
+10
-10
@@ -28,7 +28,7 @@ jobs:
|
|||||||
python-version: ["3.11", "3.12", "3.13"]
|
python-version: ["3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -177,7 +177,7 @@ jobs:
|
|||||||
name: "Mypy Version Check"
|
name: "Mypy Version Check"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ jobs:
|
|||||||
name: "Pre-commit checks"
|
name: "Pre-commit checks"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ jobs:
|
|||||||
name: "Documentation build"
|
name: "Documentation build"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@ jobs:
|
|||||||
name: "Tests and Linting - Online tests"
|
name: "Tests and Linting - Online tests"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@ jobs:
|
|||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -320,7 +320,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
jobs: ${{ toJSON(needs) }}
|
jobs: ${{ toJSON(needs) }}
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -367,7 +367,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
name: Deploy Docs through mike
|
name: Deploy Docs through mike
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
if: github.repository == 'freqtrade/freqtrade'
|
if: github.repository == 'freqtrade/freqtrade'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ jobs:
|
|||||||
if: github.repository == 'freqtrade/freqtrade'
|
if: github.repository == 'freqtrade/freqtrade'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
dockerHubDescription:
|
dockerHubDescription:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
auto-update:
|
auto-update:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: Run auto-update
|
- name: Run auto-update
|
||||||
run: pre-commit autoupdate
|
run: pre-commit autoupdate
|
||||||
|
|
||||||
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||||
add-paths: .pre-commit-config.yaml
|
add-paths: .pre-commit-config.yaml
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ jobs:
|
|||||||
# actions: read # only needed for private repos
|
# actions: read # only needed for private repos
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0
|
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ repos:
|
|||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v1.18.2"
|
rev: "v1.19.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
@@ -31,7 +31,7 @@ repos:
|
|||||||
- 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.20251115
|
- types-python-dateutil==2.9.0.20251115
|
||||||
- scipy-stubs==1.16.3.0
|
- scipy-stubs==1.16.3.1
|
||||||
- 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.6'
|
rev: 'v0.14.7'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
@@ -83,6 +83,6 @@ repos:
|
|||||||
|
|
||||||
# Ensure github actions remain safe
|
# Ensure github actions remain safe
|
||||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||||
rev: v1.16.3
|
rev: v1.18.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: zizmor
|
- id: zizmor
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ markdown==3.10
|
|||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-material==9.7.0
|
mkdocs-material==9.7.0
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.17.1
|
pymdown-extensions==10.17.2
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
mike==2.1.3
|
mike==2.1.3
|
||||||
|
|||||||
@@ -634,7 +634,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
## Custom order price rules
|
## Custom order price rules
|
||||||
|
|
||||||
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
By default, freqtrade use the orderbook to automatically set an order price ([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||||
|
|
||||||
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Freqtrade bot"""
|
"""Freqtrade bot"""
|
||||||
|
|
||||||
__version__ = "2025.11-dev"
|
__version__ = "2025.12-dev"
|
||||||
|
|
||||||
if "dev" in __version__:
|
if "dev" in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -308,11 +308,15 @@ def _download_pair_history(
|
|||||||
candle_type=candle_type,
|
candle_type=candle_type,
|
||||||
until_ms=until_ms if until_ms else None,
|
until_ms=until_ms if until_ms else None,
|
||||||
)
|
)
|
||||||
logger.info(f"Downloaded data for {pair} with length {len(new_dataframe)}.")
|
logger.info(
|
||||||
|
f"Downloaded data for {pair}, {timeframe}, {candle_type} with length "
|
||||||
|
f"{len(new_dataframe)}."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
new_dataframe = pair_candles
|
new_dataframe = pair_candles
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Downloaded data for {pair} with length {len(new_dataframe)}. Parallel Method."
|
f"Downloaded data for {pair}, {timeframe}, {candle_type} with length "
|
||||||
|
f"{len(new_dataframe)}. Parallel Method."
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.empty:
|
if data.empty:
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ from freqtrade.misc import (
|
|||||||
deep_merge_dicts,
|
deep_merge_dicts,
|
||||||
file_dump_json,
|
file_dump_json,
|
||||||
file_load_json,
|
file_load_json,
|
||||||
|
safe_value_fallback,
|
||||||
safe_value_fallback2,
|
safe_value_fallback2,
|
||||||
)
|
)
|
||||||
from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now
|
from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now
|
||||||
@@ -1119,6 +1120,7 @@ class Exchange:
|
|||||||
leverage: float,
|
leverage: float,
|
||||||
params: dict | None = None,
|
params: dict | None = None,
|
||||||
stop_loss: bool = False,
|
stop_loss: bool = False,
|
||||||
|
stop_price: float | None = None,
|
||||||
) -> CcxtOrder:
|
) -> CcxtOrder:
|
||||||
now = dt_now()
|
now = dt_now()
|
||||||
order_id = f"dry_run_{side}_{pair}_{now.timestamp()}"
|
order_id = f"dry_run_{side}_{pair}_{now.timestamp()}"
|
||||||
@@ -1145,7 +1147,7 @@ class Exchange:
|
|||||||
}
|
}
|
||||||
if stop_loss:
|
if stop_loss:
|
||||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||||
dry_order[self._ft_has["stop_price_prop"]] = dry_order["price"]
|
dry_order[self._ft_has["stop_price_prop"]] = stop_price or dry_order["price"]
|
||||||
# Workaround to avoid filling stoploss orders immediately
|
# Workaround to avoid filling stoploss orders immediately
|
||||||
dry_order["ft_order_type"] = "stoploss"
|
dry_order["ft_order_type"] = "stoploss"
|
||||||
orderbook: OrderBook | None = None
|
orderbook: OrderBook | None = None
|
||||||
@@ -1163,7 +1165,11 @@ class Exchange:
|
|||||||
|
|
||||||
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||||
# Update market order pricing
|
# Update market order pricing
|
||||||
average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
|
slippage = 0.05
|
||||||
|
worst_rate = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
|
||||||
|
average = self.get_dry_market_fill_price(
|
||||||
|
pair, side, amount, rate, worst_rate, orderbook
|
||||||
|
)
|
||||||
dry_order.update(
|
dry_order.update(
|
||||||
{
|
{
|
||||||
"average": average,
|
"average": average,
|
||||||
@@ -1203,7 +1209,13 @@ class Exchange:
|
|||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
def get_dry_market_fill_price(
|
def get_dry_market_fill_price(
|
||||||
self, pair: str, side: str, amount: float, rate: float, orderbook: OrderBook | None
|
self,
|
||||||
|
pair: str,
|
||||||
|
side: str,
|
||||||
|
amount: float,
|
||||||
|
rate: float,
|
||||||
|
worst_rate: float,
|
||||||
|
orderbook: OrderBook | None,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Get the market order fill price based on orderbook interpolation
|
Get the market order fill price based on orderbook interpolation
|
||||||
@@ -1212,8 +1224,6 @@ class Exchange:
|
|||||||
if not orderbook:
|
if not orderbook:
|
||||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||||
ob_type: OBLiteral = "asks" if side == "buy" else "bids"
|
ob_type: OBLiteral = "asks" if side == "buy" else "bids"
|
||||||
slippage = 0.05
|
|
||||||
max_slippage_val = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
|
|
||||||
|
|
||||||
remaining_amount = amount
|
remaining_amount = amount
|
||||||
filled_value = 0.0
|
filled_value = 0.0
|
||||||
@@ -1237,11 +1247,10 @@ class Exchange:
|
|||||||
forecast_avg_filled_price = max(filled_value, 0) / amount
|
forecast_avg_filled_price = max(filled_value, 0) / amount
|
||||||
# Limit max. slippage to specified value
|
# Limit max. slippage to specified value
|
||||||
if side == "buy":
|
if side == "buy":
|
||||||
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
|
forecast_avg_filled_price = min(forecast_avg_filled_price, worst_rate)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
|
forecast_avg_filled_price = max(forecast_avg_filled_price, worst_rate)
|
||||||
|
|
||||||
return self.price_to_precision(pair, forecast_avg_filled_price)
|
return self.price_to_precision(pair, forecast_avg_filled_price)
|
||||||
|
|
||||||
return rate
|
return rate
|
||||||
@@ -1253,13 +1262,15 @@ class Exchange:
|
|||||||
limit: float,
|
limit: float,
|
||||||
orderbook: OrderBook | None = None,
|
orderbook: OrderBook | None = None,
|
||||||
offset: float = 0.0,
|
offset: float = 0.0,
|
||||||
|
is_stop: bool = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self.exchange_has("fetchL2OrderBook"):
|
if not self.exchange_has("fetchL2OrderBook"):
|
||||||
return True
|
# True unless checking a stoploss order
|
||||||
|
return not is_stop
|
||||||
if not orderbook:
|
if not orderbook:
|
||||||
orderbook = self.fetch_l2_order_book(pair, 1)
|
orderbook = self.fetch_l2_order_book(pair, 1)
|
||||||
try:
|
try:
|
||||||
if side == "buy":
|
if (side == "buy" and not is_stop) or (side == "sell" and is_stop):
|
||||||
price = orderbook["asks"][0][0]
|
price = orderbook["asks"][0][0]
|
||||||
if limit * (1 - offset) >= price:
|
if limit * (1 - offset) >= price:
|
||||||
return True
|
return True
|
||||||
@@ -1278,6 +1289,38 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Check dry-run limit order fill and update fee (if it filled).
|
Check dry-run limit order fill and update fee (if it filled).
|
||||||
"""
|
"""
|
||||||
|
if order["status"] != "closed" and order.get("ft_order_type") == "stoploss":
|
||||||
|
pair = order["symbol"]
|
||||||
|
if not orderbook and self.exchange_has("fetchL2OrderBook"):
|
||||||
|
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||||
|
price = safe_value_fallback(order, self._ft_has["stop_price_prop"], "price")
|
||||||
|
crossed = self._dry_is_price_crossed(
|
||||||
|
pair, order["side"], price, orderbook, is_stop=True
|
||||||
|
)
|
||||||
|
if crossed:
|
||||||
|
average = self.get_dry_market_fill_price(
|
||||||
|
pair,
|
||||||
|
order["side"],
|
||||||
|
order["amount"],
|
||||||
|
price,
|
||||||
|
worst_rate=order["price"],
|
||||||
|
orderbook=orderbook,
|
||||||
|
)
|
||||||
|
order.update(
|
||||||
|
{
|
||||||
|
"status": "closed",
|
||||||
|
"filled": order["amount"],
|
||||||
|
"remaining": 0,
|
||||||
|
"average": average,
|
||||||
|
"cost": order["amount"] * average,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.add_dry_order_fee(
|
||||||
|
pair,
|
||||||
|
order,
|
||||||
|
"taker" if immediate else "maker",
|
||||||
|
)
|
||||||
|
return order
|
||||||
if (
|
if (
|
||||||
order["status"] != "closed"
|
order["status"] != "closed"
|
||||||
and order["type"] in ["limit"]
|
and order["type"] in ["limit"]
|
||||||
@@ -1517,8 +1560,9 @@ class Exchange:
|
|||||||
ordertype,
|
ordertype,
|
||||||
side,
|
side,
|
||||||
amount,
|
amount,
|
||||||
stop_price_norm,
|
limit_rate or stop_price_norm,
|
||||||
stop_loss=True,
|
stop_loss=True,
|
||||||
|
stop_price=stop_price_norm,
|
||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
)
|
)
|
||||||
return dry_order
|
return dry_order
|
||||||
@@ -3740,10 +3784,11 @@ class Exchange:
|
|||||||
:param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
|
:param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
|
||||||
:param futures_funding_rate: Fake funding rate to use if funding_rates are not available
|
:param futures_funding_rate: Fake funding rate to use if funding_rates are not available
|
||||||
"""
|
"""
|
||||||
|
relevant_cols = ["date", "open_mark", "open_fund"]
|
||||||
if futures_funding_rate is None:
|
if futures_funding_rate is None:
|
||||||
return mark_rates.merge(
|
return mark_rates.merge(
|
||||||
funding_rates, on="date", how="inner", suffixes=["_mark", "_fund"]
|
funding_rates, on="date", how="inner", suffixes=["_mark", "_fund"]
|
||||||
)
|
)[relevant_cols]
|
||||||
else:
|
else:
|
||||||
if len(funding_rates) == 0:
|
if len(funding_rates) == 0:
|
||||||
# No funding rate candles - full fillup with fallback variable
|
# No funding rate candles - full fillup with fallback variable
|
||||||
@@ -3756,7 +3801,7 @@ class Exchange:
|
|||||||
"low": "low_mark",
|
"low": "low_mark",
|
||||||
"volume": "volume_mark",
|
"volume": "volume_mark",
|
||||||
}
|
}
|
||||||
)
|
)[relevant_cols]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Fill up missing funding_rate candles with fallback value
|
# Fill up missing funding_rate candles with fallback value
|
||||||
@@ -3764,7 +3809,7 @@ class Exchange:
|
|||||||
funding_rates, on="date", how="left", suffixes=["_mark", "_fund"]
|
funding_rates, on="date", how="left", suffixes=["_mark", "_fund"]
|
||||||
)
|
)
|
||||||
combined["open_fund"] = combined["open_fund"].fillna(futures_funding_rate)
|
combined["open_fund"] = combined["open_fund"].fillna(futures_funding_rate)
|
||||||
return combined
|
return combined[relevant_cols]
|
||||||
|
|
||||||
def calculate_funding_fees(
|
def calculate_funding_fees(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -1063,7 +1063,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
|
def cancel_stoploss_on_exchange(self, trade: Trade, allow_nonblocking: bool = False) -> Trade:
|
||||||
|
"""
|
||||||
|
Cancels on exchange stoploss orders for the given trade.
|
||||||
|
:param trade: Trade for which to cancel stoploss order
|
||||||
|
:param allow_nonblocking: If True, will skip cancelling stoploss on exchange
|
||||||
|
if the exchange supports blocking stoploss orders.
|
||||||
|
"""
|
||||||
|
if allow_nonblocking and not self.exchange.get_option("stoploss_blocks_assets", True):
|
||||||
|
logger.info(f"Skipping cancelling stoploss on exchange for {trade}.")
|
||||||
|
return trade
|
||||||
# First cancelling stoploss on exchange ...
|
# First cancelling stoploss on exchange ...
|
||||||
for oslo in trade.open_sl_orders:
|
for oslo in trade.open_sl_orders:
|
||||||
try:
|
try:
|
||||||
@@ -2088,7 +2097,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
||||||
|
|
||||||
# First cancelling stoploss on exchange ...
|
# First cancelling stoploss on exchange ...
|
||||||
trade = self.cancel_stoploss_on_exchange(trade)
|
trade = self.cancel_stoploss_on_exchange(trade, allow_nonblocking=True)
|
||||||
|
|
||||||
order_type = ordertype or self.strategy.order_types[exit_type]
|
order_type = ordertype or self.strategy.order_types[exit_type]
|
||||||
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
|
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
|
||||||
@@ -2378,6 +2387,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.strategy.ft_stoploss_adjust(
|
self.strategy.ft_stoploss_adjust(
|
||||||
current_rate, trade, datetime.now(UTC), profit, 0, after_fill=True
|
current_rate, trade, datetime.now(UTC), profit, 0, after_fill=True
|
||||||
)
|
)
|
||||||
|
if not trade.is_open:
|
||||||
|
self.cancel_stoploss_on_exchange(trade)
|
||||||
# Updating wallets when order is closed
|
# Updating wallets when order is closed
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
return trade
|
return trade
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ from freqtrade.leverage import interest
|
|||||||
from freqtrade.misc import safe_value_fallback
|
from freqtrade.misc import safe_value_fallback
|
||||||
from freqtrade.persistence.base import ModelBase, SessionType
|
from freqtrade.persistence.base import ModelBase, SessionType
|
||||||
from freqtrade.persistence.custom_data import CustomDataWrapper, _CustomData
|
from freqtrade.persistence.custom_data import CustomDataWrapper, _CustomData
|
||||||
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none
|
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none, round_value
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -654,9 +654,10 @@ class LocalTrade:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f"Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, "
|
f"Trade(id={self.id}, pair={self.pair}, amount={round_value(self.amount, 8)}, "
|
||||||
f"is_short={self.is_short or False}, leverage={self.leverage or 1.0}, "
|
f"is_short={self.is_short or False}, "
|
||||||
f"open_rate={self.open_rate:.8f}, open_since={open_since})"
|
f"leverage={round_value(self.leverage or 1.0, 1)}, "
|
||||||
|
f"open_rate={round_value(self.open_rate, 8)}, open_since={open_since})"
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_json(self, minified: bool = False) -> dict[str, Any]:
|
def to_json(self, minified: bool = False) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -90,15 +90,16 @@ def dt_humanize_delta(dt: datetime):
|
|||||||
return humanize.naturaltime(dt)
|
return humanize.naturaltime(dt)
|
||||||
|
|
||||||
|
|
||||||
def format_date(date: datetime | None) -> str:
|
def format_date(date: datetime | None, fallback: str = "") -> str:
|
||||||
"""
|
"""
|
||||||
Return a formatted date string.
|
Return a formatted date string.
|
||||||
Returns an empty string if date is None.
|
Returns an empty string if date is None.
|
||||||
:param date: datetime to format
|
:param date: datetime to format
|
||||||
|
:param fallback: value to return if date is None
|
||||||
"""
|
"""
|
||||||
if date:
|
if date:
|
||||||
return date.strftime(DATETIME_PRINT_FORMAT)
|
return date.strftime(DATETIME_PRINT_FORMAT)
|
||||||
return ""
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
def format_ms_time(date: int | float) -> str:
|
def format_ms_time(date: int | float) -> str:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def strip_trailing_zeros(value: str) -> str:
|
|||||||
return value.rstrip("0").rstrip(".")
|
return value.rstrip("0").rstrip(".")
|
||||||
|
|
||||||
|
|
||||||
def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str:
|
def round_value(value: float | None, decimals: int, keep_trailing_zeros=False) -> str:
|
||||||
"""
|
"""
|
||||||
Round value to given decimals
|
Round value to given decimals
|
||||||
:param value: Value to be rounded
|
:param value: Value to be rounded
|
||||||
@@ -31,7 +31,7 @@ def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str:
|
|||||||
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
|
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
|
||||||
:return: Rounded value as string
|
:return: Rounded value as string
|
||||||
"""
|
"""
|
||||||
if isnan(value):
|
if value is None or isnan(value):
|
||||||
return "N/A"
|
return "N/A"
|
||||||
val = f"{value:.{decimals}f}"
|
val = f"{value:.{decimals}f}"
|
||||||
if not keep_trailing_zeros:
|
if not keep_trailing_zeros:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from freqtrade_client.ft_rest_client import FtRestClient
|
from freqtrade_client.ft_rest_client import FtRestClient
|
||||||
|
|
||||||
|
|
||||||
__version__ = "2025.11-dev"
|
__version__ = "2025.12-dev"
|
||||||
|
|
||||||
if "dev" in __version__:
|
if "dev" in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
-r requirements-freqai-rl.txt
|
-r requirements-freqai-rl.txt
|
||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
ruff==0.14.5
|
ruff==0.14.6
|
||||||
mypy==1.18.2
|
mypy==1.18.2
|
||||||
pre-commit==4.4.0
|
pre-commit==4.5.0
|
||||||
pytest==9.0.1
|
pytest==9.0.1
|
||||||
pytest-asyncio==1.3.0
|
pytest-asyncio==1.3.0
|
||||||
pytest-cov==7.0.0
|
pytest-cov==7.0.0
|
||||||
@@ -18,13 +18,13 @@ pytest-timeout==2.4.0
|
|||||||
pytest-xdist==3.8.0
|
pytest-xdist==3.8.0
|
||||||
isort==7.0.0
|
isort==7.0.0
|
||||||
# For datetime mocking
|
# For datetime mocking
|
||||||
time-machine==3.0.0
|
time-machine==3.1.0
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==7.16.6
|
nbconvert==7.16.6
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
scipy-stubs==1.16.3.0 # keep in sync with `scipy` in `requirements-hyperopt.txt`
|
scipy-stubs==1.16.3.1 # keep in sync with `scipy` in `requirements-hyperopt.txt`
|
||||||
types-cachetools==6.2.0.20251022
|
types-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
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ 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
|
||||||
lightgbm==4.6.0
|
lightgbm==4.6.0
|
||||||
xgboost==3.1.1
|
xgboost==3.1.2
|
||||||
tensorboard==2.20.0
|
tensorboard==2.20.0
|
||||||
datasieve==0.1.9
|
datasieve==0.1.9
|
||||||
|
|||||||
+3
-3
@@ -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.22
|
||||||
cryptography==46.0.3
|
cryptography==46.0.3
|
||||||
aiohttp==3.13.2
|
aiohttp==3.13.2
|
||||||
SQLAlchemy==2.0.44
|
SQLAlchemy==2.0.44
|
||||||
@@ -37,8 +37,8 @@ orjson==3.11.4
|
|||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.121.3
|
fastapi==0.122.0
|
||||||
pydantic==2.12.4
|
pydantic==2.12.5
|
||||||
uvicorn==0.38.0
|
uvicorn==0.38.0
|
||||||
pyjwt==2.10.1
|
pyjwt==2.10.1
|
||||||
aiofiles==25.1.0
|
aiofiles==25.1.0
|
||||||
|
|||||||
@@ -896,7 +896,7 @@ def test_download_pair_history_with_pair_candles(mocker, default_conf, tmp_path,
|
|||||||
assert get_historic_ohlcv_mock.call_count == 0
|
assert get_historic_ohlcv_mock.call_count == 0
|
||||||
|
|
||||||
# Verify the log message indicating parallel method was used (line 315-316)
|
# Verify the log message indicating parallel method was used (line 315-316)
|
||||||
assert log_has("Downloaded data for TEST/BTC with length 3. Parallel Method.", caplog)
|
assert log_has("Downloaded data for TEST/BTC, 5m, spot with length 3. Parallel Method.", caplog)
|
||||||
|
|
||||||
# Verify data was stored
|
# Verify data was stored
|
||||||
assert data_handler_mock.ohlcv_store.call_count == 1
|
assert data_handler_mock.ohlcv_store.call_count == 1
|
||||||
|
|||||||
@@ -157,7 +157,8 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
|
|||||||
assert "type" in order
|
assert "type" in order
|
||||||
|
|
||||||
assert order["type"] == order_type
|
assert order["type"] == order_type
|
||||||
assert order["price"] == 220
|
assert order["price"] == 217.8
|
||||||
|
assert order["stopPrice"] == 220
|
||||||
assert order["amount"] == 1
|
assert order["amount"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+117
-14
@@ -1111,21 +1111,29 @@ def test_create_dry_run_order_fees(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"side,limit,offset,expected",
|
"side,limit,offset,is_stop,expected",
|
||||||
[
|
[
|
||||||
("buy", 46.0, 0.0, True),
|
("buy", 46.0, 0.0, False, True),
|
||||||
("buy", 26.0, 0.0, True),
|
("buy", 46.0, 0.0, True, False),
|
||||||
("buy", 25.55, 0.0, False),
|
("buy", 26.0, 0.0, False, True),
|
||||||
("buy", 1, 0.0, False), # Very far away
|
("buy", 26.0, 0.0, True, False), # Stop - didn't trigger
|
||||||
("sell", 25.5, 0.0, True),
|
("buy", 25.55, 0.0, False, False),
|
||||||
("sell", 50, 0.0, False), # Very far away
|
("buy", 25.55, 0.0, True, True), # Stop - triggered
|
||||||
("sell", 25.58, 0.0, False),
|
("buy", 1, 0.0, False, False), # Very far away
|
||||||
("sell", 25.563, 0.01, False),
|
("buy", 1, 0.0, True, True), # Current price is above stop - triggered
|
||||||
("sell", 5.563, 0.01, True),
|
("sell", 25.5, 0.0, False, True),
|
||||||
|
("sell", 50, 0.0, False, False), # Very far away
|
||||||
|
("sell", 25.58, 0.0, False, False),
|
||||||
|
("sell", 25.563, 0.01, False, False),
|
||||||
|
("sell", 25.563, 0.0, True, False), # stop order - Not triggered, best bid
|
||||||
|
("sell", 25.566, 0.0, True, True), # stop order - triggered
|
||||||
|
("sell", 26, 0.01, True, True), # stop order - triggered
|
||||||
|
("sell", 5.563, 0.01, False, True),
|
||||||
|
("sell", 5.563, 0.0, True, False), # stop order - not triggered
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test__dry_is_price_crossed_with_orderbook(
|
def test__dry_is_price_crossed_with_orderbook(
|
||||||
default_conf, mocker, order_book_l2_usd, side, limit, offset, expected
|
default_conf, mocker, order_book_l2_usd, side, limit, offset, is_stop, expected
|
||||||
):
|
):
|
||||||
# Best bid 25.563
|
# Best bid 25.563
|
||||||
# Best ask 25.566
|
# Best ask 25.566
|
||||||
@@ -1134,14 +1142,14 @@ def test__dry_is_price_crossed_with_orderbook(
|
|||||||
exchange.fetch_l2_order_book = order_book_l2_usd
|
exchange.fetch_l2_order_book = order_book_l2_usd
|
||||||
orderbook = order_book_l2_usd.return_value
|
orderbook = order_book_l2_usd.return_value
|
||||||
result = exchange._dry_is_price_crossed(
|
result = exchange._dry_is_price_crossed(
|
||||||
"LTC/USDT", side, limit, orderbook=orderbook, offset=offset
|
"LTC/USDT", side, limit, orderbook=orderbook, offset=offset, is_stop=is_stop
|
||||||
)
|
)
|
||||||
assert result is expected
|
assert result is expected
|
||||||
assert order_book_l2_usd.call_count == 0
|
assert order_book_l2_usd.call_count == 0
|
||||||
|
|
||||||
# Test without passing orderbook
|
# Test without passing orderbook
|
||||||
order_book_l2_usd.reset_mock()
|
order_book_l2_usd.reset_mock()
|
||||||
result = exchange._dry_is_price_crossed("LTC/USDT", side, limit, offset=offset)
|
result = exchange._dry_is_price_crossed("LTC/USDT", side, limit, offset=offset, is_stop=is_stop)
|
||||||
assert result is expected
|
assert result is expected
|
||||||
|
|
||||||
|
|
||||||
@@ -1165,7 +1173,10 @@ def test__dry_is_price_crossed_without_orderbook_support(default_conf, mocker):
|
|||||||
exchange.fetch_l2_order_book = MagicMock()
|
exchange.fetch_l2_order_book = MagicMock()
|
||||||
mocker.patch(f"{EXMS}.exchange_has", return_value=False)
|
mocker.patch(f"{EXMS}.exchange_has", return_value=False)
|
||||||
assert exchange._dry_is_price_crossed("LTC/USDT", "buy", 1.0)
|
assert exchange._dry_is_price_crossed("LTC/USDT", "buy", 1.0)
|
||||||
|
assert exchange._dry_is_price_crossed("LTC/USDT", "sell", 1.0)
|
||||||
assert exchange.fetch_l2_order_book.call_count == 0
|
assert exchange.fetch_l2_order_book.call_count == 0
|
||||||
|
assert not exchange._dry_is_price_crossed("LTC/USDT", "buy", 1.0, is_stop=True)
|
||||||
|
assert not exchange._dry_is_price_crossed("LTC/USDT", "sell", 1.0, is_stop=True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -1176,7 +1187,7 @@ def test__dry_is_price_crossed_without_orderbook_support(default_conf, mocker):
|
|||||||
(False, False, "sell", 1.0, "open", None, 0, None),
|
(False, False, "sell", 1.0, "open", None, 0, None),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_check_dry_limit_order_filled_parametrized(
|
def test_check_dry_limit_order_filled(
|
||||||
default_conf,
|
default_conf,
|
||||||
mocker,
|
mocker,
|
||||||
crossed,
|
crossed,
|
||||||
@@ -1220,6 +1231,70 @@ def test_check_dry_limit_order_filled_parametrized(
|
|||||||
assert fee_mock.call_count == expected_calls
|
assert fee_mock.call_count == expected_calls
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"immediate,crossed,expected_status,expected_fee_type",
|
||||||
|
[
|
||||||
|
(True, True, "closed", "taker"),
|
||||||
|
(False, True, "closed", "maker"),
|
||||||
|
(True, False, "open", None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_check_dry_limit_order_filled_stoploss(
|
||||||
|
default_conf, mocker, immediate, crossed, expected_status, expected_fee_type, order_book_l2_usd
|
||||||
|
):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
EXMS,
|
||||||
|
exchange_has=MagicMock(return_value=True),
|
||||||
|
_dry_is_price_crossed=MagicMock(return_value=crossed),
|
||||||
|
fetch_l2_order_book=order_book_l2_usd,
|
||||||
|
)
|
||||||
|
average_mock = mocker.patch(f"{EXMS}.get_dry_market_fill_price", return_value=24.25)
|
||||||
|
fee_mock = mocker.patch(
|
||||||
|
f"{EXMS}.add_dry_order_fee",
|
||||||
|
autospec=True,
|
||||||
|
side_effect=lambda self, pair, dry_order, taker_or_maker: dry_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
amount = 1.75
|
||||||
|
order = {
|
||||||
|
"symbol": "LTC/USDT",
|
||||||
|
"status": "open",
|
||||||
|
"type": "limit",
|
||||||
|
"side": "sell",
|
||||||
|
"amount": amount,
|
||||||
|
"filled": 0.0,
|
||||||
|
"remaining": amount,
|
||||||
|
"price": 25.0,
|
||||||
|
"average": 0.0,
|
||||||
|
"cost": 0.0,
|
||||||
|
"fee": None,
|
||||||
|
"ft_order_type": "stoploss",
|
||||||
|
"stopLossPrice": 24.5,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = exchange.check_dry_limit_order_filled(order, immediate=immediate)
|
||||||
|
|
||||||
|
assert result["status"] == expected_status
|
||||||
|
assert order_book_l2_usd.call_count == 1
|
||||||
|
if crossed:
|
||||||
|
assert result["filled"] == amount
|
||||||
|
assert result["remaining"] == 0
|
||||||
|
assert result["average"] == 24.25
|
||||||
|
assert result["cost"] == pytest.approx(amount * 24.25)
|
||||||
|
assert average_mock.call_count == 1
|
||||||
|
assert fee_mock.call_count == 1
|
||||||
|
assert fee_mock.call_args[0][1] == "LTC/USDT"
|
||||||
|
assert fee_mock.call_args[0][3] == expected_fee_type
|
||||||
|
else:
|
||||||
|
assert result["filled"] == 0.0
|
||||||
|
assert result["remaining"] == amount
|
||||||
|
assert result["average"] == 0.0
|
||||||
|
|
||||||
|
assert average_mock.call_count == 0
|
||||||
|
assert fee_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"side,price,filled,converted",
|
"side,price,filled,converted",
|
||||||
[
|
[
|
||||||
@@ -5229,6 +5304,7 @@ def test_combine_funding_and_mark(
|
|||||||
{"date": trade_date, "open": mark_price},
|
{"date": trade_date, "open": mark_price},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
# Test fallback to futures funding rate for missing funding rates
|
||||||
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
|
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
|
||||||
|
|
||||||
if futures_funding_rate is not None:
|
if futures_funding_rate is not None:
|
||||||
@@ -5256,6 +5332,33 @@ def test_combine_funding_and_mark(
|
|||||||
|
|
||||||
assert len(df) == 0
|
assert len(df) == 0
|
||||||
|
|
||||||
|
# Test fallback to futures funding rate for middle missing funding rate
|
||||||
|
funding_rates = DataFrame(
|
||||||
|
[
|
||||||
|
{"date": prior2_date, "open": funding_rate},
|
||||||
|
# missing 1 hour
|
||||||
|
{"date": trade_date, "open": funding_rate},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
mark_rates = DataFrame(
|
||||||
|
[
|
||||||
|
{"date": prior2_date, "open": mark_price},
|
||||||
|
{"date": prior_date, "open": mark_price},
|
||||||
|
{"date": trade_date, "open": mark_price},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
|
||||||
|
|
||||||
|
if futures_funding_rate is not None:
|
||||||
|
assert len(df) == 3
|
||||||
|
assert df.iloc[0]["open_fund"] == funding_rate
|
||||||
|
assert df.iloc[1]["open_fund"] == futures_funding_rate
|
||||||
|
assert df.iloc[2]["open_fund"] == funding_rate
|
||||||
|
assert df["date"].to_list() == [prior2_date, prior_date, trade_date]
|
||||||
|
else:
|
||||||
|
assert len(df) == 2
|
||||||
|
assert df["date"].to_list() == [prior2_date, trade_date]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"exchange,rate_start,rate_end,d1,d2,amount,expected_fees",
|
"exchange,rate_start,rate_end,d1,d2,amount,expected_fees",
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ def test_create_stoploss_order_dry_run_htx(default_conf, mocker):
|
|||||||
assert "type" in order
|
assert "type" in order
|
||||||
|
|
||||||
assert order["type"] == order_type
|
assert order["type"] == order_type
|
||||||
assert order["price"] == 220
|
assert order["price"] == 217.8
|
||||||
|
assert order["stopPrice"] == 220
|
||||||
assert order["amount"] == 1
|
assert order["amount"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2548,9 +2548,9 @@ def test_manage_open_orders_exception(
|
|||||||
caplog.clear()
|
caplog.clear()
|
||||||
freqtrade.manage_open_orders()
|
freqtrade.manage_open_orders()
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
r"Cannot query order for Trade\(id=1, pair=ADA/USDT, amount=30.00000000, "
|
r"Cannot query order for Trade\(id=1, pair=ADA/USDT, amount=30, "
|
||||||
r"is_short=False, leverage=1.0, "
|
r"is_short=False, leverage=1, "
|
||||||
r"open_rate=2.00000000, open_since="
|
r"open_rate=2, open_since="
|
||||||
f"{open_trade_usdt.open_date.strftime('%Y-%m-%d %H:%M:%S')}"
|
f"{open_trade_usdt.open_date.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||||
r"\) due to Traceback \(most recent call last\):\n*",
|
r"\) due to Traceback \(most recent call last\):\n*",
|
||||||
caplog,
|
caplog,
|
||||||
@@ -3751,8 +3751,8 @@ def test_get_real_amount_quote(
|
|||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == (amount * 0.001)
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == (amount * 0.001)
|
||||||
assert log_has(
|
assert log_has(
|
||||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,"
|
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, is_short=False,"
|
||||||
" leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.008.",
|
" leverage=1, open_rate=0.245441, open_since=closed), fee=0.008.",
|
||||||
caplog,
|
caplog,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3805,8 +3805,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
|
|||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) is None
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) is None
|
||||||
assert log_has(
|
assert log_has(
|
||||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
|
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
|
||||||
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) failed: "
|
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed) failed: "
|
||||||
"myTrade-dict empty found",
|
"myTrade-dict empty found",
|
||||||
caplog,
|
caplog,
|
||||||
)
|
)
|
||||||
@@ -3825,8 +3825,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
|
|||||||
0,
|
0,
|
||||||
True,
|
True,
|
||||||
(
|
(
|
||||||
"Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False, "
|
"Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8, is_short=False, "
|
||||||
"leverage=1.0, open_rate=0.24544100, open_since=closed) [buy]: 0.00094518 BNB -"
|
"leverage=1, open_rate=0.245441, open_since=closed) [buy]: 0.00094518 BNB -"
|
||||||
" rate: None"
|
" rate: None"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -3836,8 +3836,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
|
|||||||
0.004,
|
0.004,
|
||||||
False,
|
False,
|
||||||
(
|
(
|
||||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
|
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
|
||||||
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.004."
|
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed), fee=0.004."
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
# invalid, no currency in from fee dict
|
# invalid, no currency in from fee dict
|
||||||
@@ -3941,8 +3941,8 @@ def test_get_real_amount_multi(
|
|||||||
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == expected_amount
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == expected_amount
|
||||||
assert log_has(
|
assert log_has(
|
||||||
(
|
(
|
||||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
|
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
|
||||||
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), "
|
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed), "
|
||||||
f"fee={expected_amount}."
|
f"fee={expected_amount}."
|
||||||
),
|
),
|
||||||
caplog,
|
caplog,
|
||||||
|
|||||||
@@ -50,7 +50,14 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
stoploss_order_mock = MagicMock(side_effect=stop_orders)
|
stoploss_order_mock = MagicMock(side_effect=stop_orders)
|
||||||
# Sell 3rd trade (not called for the first trade)
|
# Sell 3rd trade (not called for the first trade)
|
||||||
should_sell_mock = MagicMock(side_effect=[[], [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]])
|
should_sell_mock = MagicMock(side_effect=[[], [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]])
|
||||||
cancel_order_mock = MagicMock()
|
|
||||||
|
def patch_stoploss(order_id, *args, **kwargs):
|
||||||
|
slo = stoploss_order_open.copy()
|
||||||
|
slo["id"] = order_id
|
||||||
|
slo["status"] = "canceled"
|
||||||
|
return slo
|
||||||
|
|
||||||
|
cancel_order_mock = MagicMock(side_effect=patch_stoploss)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
EXMS,
|
EXMS,
|
||||||
create_stoploss=stoploss,
|
create_stoploss=stoploss,
|
||||||
@@ -793,9 +800,13 @@ def test_dca_handle_similar_open_order(
|
|||||||
# Should Create a new exit order
|
# Should Create a new exit order
|
||||||
freqtrade.exchange.amount_to_contract_precision = MagicMock(return_value=2)
|
freqtrade.exchange.amount_to_contract_precision = MagicMock(return_value=2)
|
||||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-2)
|
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-2)
|
||||||
|
msg = r"Skipping cancelling stoploss on exchange for.*"
|
||||||
|
|
||||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
|
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
|
||||||
|
assert not log_has_re(msg, caplog)
|
||||||
freqtrade.process()
|
freqtrade.process()
|
||||||
|
assert log_has_re(msg, caplog)
|
||||||
|
|
||||||
trade = Trade.get_trades().first()
|
trade = Trade.get_trades().first()
|
||||||
|
|
||||||
assert trade.orders[-2].status == "closed"
|
assert trade.orders[-2].status == "closed"
|
||||||
|
|||||||
@@ -372,8 +372,8 @@ def test_borrowed(fee, is_short, lev, borrowed, trading_mode):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"is_short,open_rate,close_rate,lev,profit,trading_mode",
|
"is_short,open_rate,close_rate,lev,profit,trading_mode",
|
||||||
[
|
[
|
||||||
(False, 2.0, 2.2, 1.0, 0.09451372, spot),
|
(False, 2, 2.2, 1, 0.09451372, spot),
|
||||||
(True, 2.2, 2.0, 3.0, 0.25894253, margin),
|
(True, 2.2, 2.0, 3, 0.25894253, margin),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@@ -493,8 +493,8 @@ def test_update_limit_order(
|
|||||||
assert trade.close_date is None
|
assert trade.close_date is None
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
f"LIMIT_{entry_side.upper()} has been fulfilled for "
|
f"LIMIT_{entry_side.upper()} has been fulfilled for "
|
||||||
r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
|
r"Trade\(id=2, pair=ADA/USDT, amount=30, "
|
||||||
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, "
|
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}, "
|
||||||
r"open_since=.*\).",
|
r"open_since=.*\).",
|
||||||
caplog,
|
caplog,
|
||||||
)
|
)
|
||||||
@@ -511,8 +511,8 @@ def test_update_limit_order(
|
|||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
f"LIMIT_{exit_side.upper()} has been fulfilled for "
|
f"LIMIT_{exit_side.upper()} has been fulfilled for "
|
||||||
r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
|
r"Trade\(id=2, pair=ADA/USDT, amount=30, "
|
||||||
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, "
|
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}, "
|
||||||
r"open_since=.*\).",
|
r"open_since=.*\).",
|
||||||
caplog,
|
caplog,
|
||||||
)
|
)
|
||||||
@@ -545,8 +545,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
|
|||||||
assert trade.close_date is None
|
assert trade.close_date is None
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
r"MARKET_BUY has been fulfilled for Trade\(id=1, "
|
r"MARKET_BUY has been fulfilled for Trade\(id=1, "
|
||||||
r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, "
|
r"pair=ADA/USDT, amount=30, is_short=False, leverage=1, "
|
||||||
r"open_rate=2.00000000, open_since=.*\).",
|
r"open_rate=2, open_since=.*\).",
|
||||||
caplog,
|
caplog,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -561,8 +561,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
|
|||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
r"MARKET_SELL has been fulfilled for Trade\(id=1, "
|
r"MARKET_SELL has been fulfilled for Trade\(id=1, "
|
||||||
r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, "
|
r"pair=ADA/USDT, amount=30, is_short=False, leverage=1, "
|
||||||
r"open_rate=2.00000000, open_since=.*\).",
|
r"open_rate=2, open_since=.*\).",
|
||||||
caplog,
|
caplog,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ def test_format_date() -> None:
|
|||||||
date = datetime(2021, 9, 30, 22, 59, 3, 455555, tzinfo=UTC)
|
date = datetime(2021, 9, 30, 22, 59, 3, 455555, tzinfo=UTC)
|
||||||
assert format_date(date) == "2021-09-30 22:59:03"
|
assert format_date(date) == "2021-09-30 22:59:03"
|
||||||
assert format_date(None) == ""
|
assert format_date(None) == ""
|
||||||
|
assert format_date(None, "closed") == "closed"
|
||||||
|
|
||||||
|
|
||||||
def test_format_ms_time_detailed() -> None:
|
def test_format_ms_time_detailed() -> None:
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ def test_round_value():
|
|||||||
assert round_value(222.2, 0, True) == "222"
|
assert round_value(222.2, 0, True) == "222"
|
||||||
assert round_value(float("nan"), 0, True) == "N/A"
|
assert round_value(float("nan"), 0, True) == "N/A"
|
||||||
assert round_value(float("nan"), 10, True) == "N/A"
|
assert round_value(float("nan"), 10, True) == "N/A"
|
||||||
|
assert round_value(None, 10, True) == "N/A"
|
||||||
|
assert round_value(None, 1, True) == "N/A"
|
||||||
|
|
||||||
|
|
||||||
def test_format_duration():
|
def test_format_duration():
|
||||||
|
|||||||
Reference in New Issue
Block a user