feat(exchange): add Kraken Futures exchange class
This commit is contained in:
@@ -0,0 +1,611 @@
|
|||||||
|
"""Kraken Futures exchange subclass"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ccxt.base.errors import NotSupported, OrderNotFound
|
||||||
|
|
||||||
|
from freqtrade.constants import BuySell
|
||||||
|
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange.common import retrier
|
||||||
|
from freqtrade.exchange.exchange import Exchange
|
||||||
|
from freqtrade.exchange.exchange_types import FtHas
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KrakenFutures(Exchange):
|
||||||
|
"""Kraken Futures exchange class.
|
||||||
|
|
||||||
|
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||||
|
|
||||||
|
Key differences from spot Kraken:
|
||||||
|
- CCXT does not implement fetchOrder; we emulate via open/closed/history endpoints
|
||||||
|
- Stop orders use triggerPrice/triggerSignal instead of stopPrice
|
||||||
|
- Multi-collateral accounts require synthetic USD balance from flex account
|
||||||
|
- OHLCV limit capped at 2000 candles
|
||||||
|
"""
|
||||||
|
|
||||||
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
|
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Freqtrade uses _ft_has (exchange_has_overrides) for config validation.
|
||||||
|
_ft_has: FtHas = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {
|
||||||
|
"limit": "limit",
|
||||||
|
"market": "market",
|
||||||
|
},
|
||||||
|
# request param used by Exchange._get_stop_params
|
||||||
|
"stop_price_param": "triggerPrice",
|
||||||
|
# response key used by stoploss_adjust and general stop parsing
|
||||||
|
"stop_price_prop": "stopPrice",
|
||||||
|
# optional futures price type mapping (only used if stoploss_price_type is configured)
|
||||||
|
"stop_price_type_field": "triggerSignal",
|
||||||
|
"stop_price_type_value_mapping": {
|
||||||
|
PriceType.LAST: "last",
|
||||||
|
PriceType.MARK: "mark",
|
||||||
|
PriceType.INDEX: "index",
|
||||||
|
},
|
||||||
|
# override ccxt has-gaps
|
||||||
|
"exchange_has_overrides": {
|
||||||
|
"fetchOrder": True,
|
||||||
|
"createMarketOrder": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_ft_has() -> dict[str, Any]:
|
||||||
|
# Kept for compatibility with older tests and helper code.
|
||||||
|
return {
|
||||||
|
"fetchOrder": True,
|
||||||
|
"createMarketOrder": True,
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {
|
||||||
|
"limit": "limit",
|
||||||
|
"market": "market",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_balances(self, params: dict | None = None) -> dict[str, Any]:
|
||||||
|
balances = super().get_balances(params=params)
|
||||||
|
|
||||||
|
stake = str(self._config.get("stake_currency", "")).upper()
|
||||||
|
if stake != "USD":
|
||||||
|
return balances
|
||||||
|
|
||||||
|
flex = self._get_flex_account(balances, params)
|
||||||
|
if flex is None:
|
||||||
|
return balances
|
||||||
|
|
||||||
|
usd_free, usd_total = self._extract_usd_from_flex(flex)
|
||||||
|
if usd_free is None or usd_total is None:
|
||||||
|
return balances
|
||||||
|
|
||||||
|
# Preserve existing USD if higher (usually dust)
|
||||||
|
if isinstance(balances.get("free"), dict):
|
||||||
|
existing = self._safe_float(balances["free"].get("USD"))
|
||||||
|
if existing is not None:
|
||||||
|
usd_free = max(existing, usd_free)
|
||||||
|
|
||||||
|
self._apply_usd_balances(balances, usd_free, usd_total)
|
||||||
|
return balances
|
||||||
|
|
||||||
|
def _get_flex_account(
|
||||||
|
self, balances: dict[str, Any], params: dict | None
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Extract flex account from balances or fetch directly."""
|
||||||
|
flex = self._extract_flex_from_raw(balances)
|
||||||
|
if flex is not None:
|
||||||
|
return flex
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = self._api.fetch_balance(params or {})
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return self._extract_flex_from_raw(raw)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_flex_from_raw(raw: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||||
|
"""Navigate raw -> info -> accounts -> flex."""
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
info = raw.get("info")
|
||||||
|
if not isinstance(info, dict):
|
||||||
|
return None
|
||||||
|
accounts = info.get("accounts")
|
||||||
|
if not isinstance(accounts, dict):
|
||||||
|
return None
|
||||||
|
flex = accounts.get("flex")
|
||||||
|
return flex if isinstance(flex, dict) else None
|
||||||
|
|
||||||
|
def _extract_usd_from_flex(self, flex: dict[str, Any]) -> tuple[float | None, float | None]:
|
||||||
|
"""Extract USD free and total from flex account."""
|
||||||
|
usd_free = self._safe_float(flex.get("availableMargin") or flex.get("available_margin"))
|
||||||
|
usd_total = self._safe_float(
|
||||||
|
flex.get("balanceValue") or flex.get("collateralValue") or flex.get("portfolioValue")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fallback: sum currencies[*].value
|
||||||
|
if usd_total is None:
|
||||||
|
usd_total = self._sum_currencies_value(flex.get("currencies"))
|
||||||
|
|
||||||
|
# Fill missing with the other if available
|
||||||
|
if usd_total is None and usd_free is not None:
|
||||||
|
usd_total = usd_free
|
||||||
|
if usd_free is None and usd_total is not None:
|
||||||
|
usd_free = usd_total
|
||||||
|
|
||||||
|
return usd_free, usd_total
|
||||||
|
|
||||||
|
def _sum_currencies_value(self, currencies: Any) -> float | None:
|
||||||
|
"""Sum value fields from currencies dict."""
|
||||||
|
if not isinstance(currencies, dict):
|
||||||
|
return None
|
||||||
|
total = 0.0
|
||||||
|
found = False
|
||||||
|
for cur in currencies.values():
|
||||||
|
if isinstance(cur, dict):
|
||||||
|
v = self._safe_float(cur.get("value"))
|
||||||
|
if v is not None:
|
||||||
|
total += v
|
||||||
|
found = True
|
||||||
|
return total if found else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _apply_usd_balances(balances: dict[str, Any], usd_free: float, usd_total: float) -> None:
|
||||||
|
"""Update balances dict with USD values."""
|
||||||
|
balances["USD"] = {"free": usd_free, "used": 0.0, "total": usd_total}
|
||||||
|
balances.setdefault("free", {})
|
||||||
|
balances.setdefault("used", {})
|
||||||
|
balances.setdefault("total", {})
|
||||||
|
|
||||||
|
if isinstance(balances["free"], dict):
|
||||||
|
balances["free"]["USD"] = usd_free
|
||||||
|
if isinstance(balances["used"], dict):
|
||||||
|
balances["used"]["USD"] = 0.0
|
||||||
|
if isinstance(balances["total"], dict):
|
||||||
|
balances["total"]["USD"] = usd_total
|
||||||
|
|
||||||
|
def validate_stakecurrency(self, stake_currency: str) -> None:
|
||||||
|
# Kraken Futures multi-collateral allows EUR collateral even if markets look USD-settled.
|
||||||
|
if str(stake_currency).upper() == "EUR":
|
||||||
|
return
|
||||||
|
super().validate_stakecurrency(stake_currency)
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def fetch_order(
|
||||||
|
self,
|
||||||
|
order_id: str,
|
||||||
|
pair: str,
|
||||||
|
params: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
params = params or {}
|
||||||
|
|
||||||
|
order = self._fetch_order_default(order_id, pair, params)
|
||||||
|
if order is not None:
|
||||||
|
return self._normalize_fetched_order(order)
|
||||||
|
|
||||||
|
order = self._fetch_order_scan_open_closed(order_id, pair, params)
|
||||||
|
if order is not None:
|
||||||
|
return self._normalize_fetched_order(order)
|
||||||
|
|
||||||
|
order = self._fetch_order_from_history(order_id, pair, params)
|
||||||
|
if order is not None:
|
||||||
|
return self._normalize_fetched_order(order)
|
||||||
|
|
||||||
|
# Do not crash the bot during startup if the exchange cannot find the order.
|
||||||
|
now_ms = int(getattr(self._api, "milliseconds", lambda: int(time.time() * 1000))())
|
||||||
|
try:
|
||||||
|
iso = self._api.iso8601(now_ms)
|
||||||
|
except Exception:
|
||||||
|
iso = None
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"Order not found on exchange, returning pseudo order to avoid crash. id=%s pair=%s",
|
||||||
|
order_id,
|
||||||
|
pair,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": order_id,
|
||||||
|
"symbol": pair,
|
||||||
|
"status": "open",
|
||||||
|
"side": None,
|
||||||
|
"type": None,
|
||||||
|
"price": 0.0,
|
||||||
|
"amount": 0.0,
|
||||||
|
"filled": 0.0,
|
||||||
|
"remaining": 0.0,
|
||||||
|
"timestamp": now_ms,
|
||||||
|
"datetime": iso,
|
||||||
|
"info": {"_ft_note": "Order not found via API, pseudo order returned"},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_funding_fees(self, pair: str, amount: float, is_short: bool, open_date):
|
||||||
|
try:
|
||||||
|
return super().get_funding_fees(pair, amount, is_short, open_date)
|
||||||
|
except OperationalException as e:
|
||||||
|
if "fetch_funding_history" in str(e):
|
||||||
|
logger.debug("Kraken Futures: fetch_funding_history unsupported, returning 0.")
|
||||||
|
return 0.0
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _fetch_order_default(
|
||||||
|
self, order_id: str, pair: str, params: dict[str, Any]
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
try:
|
||||||
|
return self._api.fetch_order(order_id, pair, params)
|
||||||
|
except (NotSupported, AttributeError, OrderNotFound):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter_params_for_open_closed(params: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
if not params:
|
||||||
|
return {}
|
||||||
|
blacklist = {"since", "before", "from", "to"}
|
||||||
|
return {k: v for k, v in params.items() if k not in blacklist}
|
||||||
|
|
||||||
|
def _fetch_order_scan_open_closed(
|
||||||
|
self, order_id: str, pair: str, params: dict[str, Any]
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
list_params = self._filter_params_for_open_closed(params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
open_orders = self.fetch_open_orders(pair, params=list_params)
|
||||||
|
except Exception:
|
||||||
|
open_orders = []
|
||||||
|
|
||||||
|
for o in open_orders:
|
||||||
|
if self._contains_value(o, order_id):
|
||||||
|
return o
|
||||||
|
|
||||||
|
try:
|
||||||
|
closed_orders = self.fetch_closed_orders(pair, params=list_params)
|
||||||
|
except Exception:
|
||||||
|
closed_orders = []
|
||||||
|
|
||||||
|
for o in closed_orders:
|
||||||
|
if self._contains_value(o, order_id):
|
||||||
|
return o
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _fetch_order_from_history(
|
||||||
|
self, order_id: str, pair: str, params: dict[str, Any]
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
# Kraken Futures has separate history feeds for orders and triggers.
|
||||||
|
for method_name in ("historyGetOrders", "historyGetTriggers"):
|
||||||
|
order = self._fetch_order_from_history_method(method_name, order_id, pair, params)
|
||||||
|
if order is not None:
|
||||||
|
return order
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _fetch_order_from_history_method(
|
||||||
|
self,
|
||||||
|
method_name: str,
|
||||||
|
order_id: str,
|
||||||
|
pair: str,
|
||||||
|
params: dict[str, Any],
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
if not hasattr(self._api, method_name):
|
||||||
|
return None
|
||||||
|
|
||||||
|
hist_params = dict(params)
|
||||||
|
if not any(k in hist_params for k in ("since", "before", "from", "to")):
|
||||||
|
now_ms = int(getattr(self._api, "milliseconds", lambda: int(time.time() * 1000))())
|
||||||
|
hist_params["since"] = now_ms - 48 * 60 * 60 * 1000 # 48 hours lookback
|
||||||
|
|
||||||
|
try:
|
||||||
|
hist = getattr(self._api, method_name)(hist_params)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
elements = self._extract_history_elements(hist)
|
||||||
|
return self._parse_order_from_history_elements(elements, order_id, pair)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_history_elements(hist: Any) -> list[dict[str, Any]]:
|
||||||
|
if isinstance(hist, list):
|
||||||
|
return [x for x in hist if isinstance(x, dict)]
|
||||||
|
|
||||||
|
if not isinstance(hist, dict):
|
||||||
|
return []
|
||||||
|
|
||||||
|
if isinstance(hist.get("elements"), list):
|
||||||
|
return [x for x in hist["elements"] if isinstance(x, dict)]
|
||||||
|
|
||||||
|
result = hist.get("result")
|
||||||
|
if isinstance(result, dict) and isinstance(result.get("elements"), list):
|
||||||
|
return [x for x in result["elements"] if isinstance(x, dict)]
|
||||||
|
|
||||||
|
events = hist.get("events")
|
||||||
|
if isinstance(events, list):
|
||||||
|
return [x for x in events if isinstance(x, dict)]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _parse_order_from_history_elements(
|
||||||
|
self, elements: list[dict[str, Any]], order_id: str, pair: str
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
for el in elements:
|
||||||
|
event = el.get("event") or el.get("events") or {}
|
||||||
|
if not isinstance(event, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for event_name, payload in event.items():
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
orderish = self._extract_orderish(payload)
|
||||||
|
uid = self._extract_uid(orderish, payload)
|
||||||
|
|
||||||
|
if uid is None and self._contains_value(payload, order_id):
|
||||||
|
uid = order_id
|
||||||
|
|
||||||
|
if str(uid) != str(order_id):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return self._build_ccxt_like_order_from_history(
|
||||||
|
el, str(event_name), orderish, order_id, pair
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_orderish(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
for key in ("order", "trigger", "triggerOrder"):
|
||||||
|
v = payload.get(key)
|
||||||
|
if isinstance(v, dict):
|
||||||
|
return v
|
||||||
|
return payload
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_uid(orderish: dict[str, Any], payload: dict[str, Any]) -> Any:
|
||||||
|
keys = (
|
||||||
|
"uid",
|
||||||
|
"id",
|
||||||
|
"orderId",
|
||||||
|
"order_id",
|
||||||
|
"orderUid",
|
||||||
|
"triggerId",
|
||||||
|
"trigger_id",
|
||||||
|
"triggerUid",
|
||||||
|
"triggerOrderUid",
|
||||||
|
)
|
||||||
|
for k in keys:
|
||||||
|
if k in orderish:
|
||||||
|
return orderish.get(k)
|
||||||
|
for k in keys:
|
||||||
|
if k in payload:
|
||||||
|
return payload.get(k)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _contains_value(obj: Any, needle: str) -> bool:
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return any(KrakenFutures._contains_value(v, needle) for v in obj.values())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return any(KrakenFutures._contains_value(v, needle) for v in obj)
|
||||||
|
return str(obj) == str(needle)
|
||||||
|
|
||||||
|
def _build_ccxt_like_order_from_history(
|
||||||
|
self,
|
||||||
|
el: dict[str, Any],
|
||||||
|
event_name: str,
|
||||||
|
order: dict[str, Any],
|
||||||
|
order_id: str,
|
||||||
|
pair: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
status = self._map_history_event_to_status(event_name)
|
||||||
|
|
||||||
|
amount = self._safe_float(order.get("quantity") or order.get("qty"))
|
||||||
|
filled = self._safe_float(
|
||||||
|
order.get("filled") or order.get("filledQty") or order.get("filled_qty")
|
||||||
|
)
|
||||||
|
price = self._safe_float(order.get("limitPrice") or order.get("price"))
|
||||||
|
stop_price = self._safe_float(
|
||||||
|
order.get("stopPrice")
|
||||||
|
or order.get("triggerPrice")
|
||||||
|
or order.get("trigger_price")
|
||||||
|
or order.get("stop_price")
|
||||||
|
)
|
||||||
|
|
||||||
|
side_raw = str(order.get("direction") or order.get("side") or "").lower()
|
||||||
|
if not side_raw and isinstance(order.get("buy"), bool):
|
||||||
|
side_raw = "buy" if order["buy"] else "sell"
|
||||||
|
side = "buy" if side_raw == "buy" else "sell" if side_raw == "sell" else None
|
||||||
|
|
||||||
|
order_type = self._infer_order_type_from_history(order, price)
|
||||||
|
|
||||||
|
remaining = None
|
||||||
|
if amount is not None and filled is not None:
|
||||||
|
remaining = max(amount - filled, 0.0)
|
||||||
|
|
||||||
|
ts = order.get("timestamp") or order.get("time") or el.get("timestamp") or el.get("time")
|
||||||
|
ts_int = int(ts) if ts is not None else None
|
||||||
|
|
||||||
|
reduce_only = order.get("reduceOnly")
|
||||||
|
if isinstance(reduce_only, str):
|
||||||
|
reduce_only = reduce_only.lower() == "true"
|
||||||
|
|
||||||
|
result: dict[str, Any] = {
|
||||||
|
"id": order_id,
|
||||||
|
"symbol": pair,
|
||||||
|
"status": status,
|
||||||
|
"side": side,
|
||||||
|
"type": order_type,
|
||||||
|
"price": price,
|
||||||
|
"amount": amount,
|
||||||
|
"filled": filled,
|
||||||
|
"remaining": remaining,
|
||||||
|
"timestamp": ts_int,
|
||||||
|
"datetime": self._api.iso8601(ts_int) if ts_int is not None else None,
|
||||||
|
"info": el,
|
||||||
|
}
|
||||||
|
|
||||||
|
if stop_price is not None:
|
||||||
|
result["stopPrice"] = stop_price
|
||||||
|
|
||||||
|
if isinstance(reduce_only, bool):
|
||||||
|
result["reduceOnly"] = reduce_only
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _infer_order_type_from_history(order: dict[str, Any], price: float | None) -> str | None:
|
||||||
|
raw = str(order.get("orderType") or order.get("type") or "").lower()
|
||||||
|
|
||||||
|
if raw in ("lmt", "limit", "post", "ioc"):
|
||||||
|
return "limit"
|
||||||
|
if raw in ("mkt", "market"):
|
||||||
|
return "market"
|
||||||
|
|
||||||
|
if raw in (
|
||||||
|
"stp",
|
||||||
|
"stop",
|
||||||
|
"take_profit",
|
||||||
|
"takeprofit",
|
||||||
|
"take-profit",
|
||||||
|
"trailing_stop",
|
||||||
|
"trailingstop",
|
||||||
|
):
|
||||||
|
return "limit" if price is not None else "market"
|
||||||
|
|
||||||
|
if price is not None:
|
||||||
|
return "limit"
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _map_history_event_to_status(event_name: str) -> str:
|
||||||
|
name = (event_name or "").lower()
|
||||||
|
if "cancel" in name:
|
||||||
|
return "canceled"
|
||||||
|
if "reject" in name:
|
||||||
|
return "rejected"
|
||||||
|
if "place" in name:
|
||||||
|
return "open"
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _safe_float(v: Any) -> float | None:
|
||||||
|
try:
|
||||||
|
if v is None or v == "":
|
||||||
|
return None
|
||||||
|
return float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _find_first_value(obj: Any, keys: set[str]) -> Any | None:
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
for k, v in obj.items():
|
||||||
|
if k in keys and v not in (None, ""):
|
||||||
|
return v
|
||||||
|
found = KrakenFutures._find_first_value(v, keys)
|
||||||
|
if found is not None:
|
||||||
|
return found
|
||||||
|
return None
|
||||||
|
if isinstance(obj, list):
|
||||||
|
for v in obj:
|
||||||
|
found = KrakenFutures._find_first_value(v, keys)
|
||||||
|
if found is not None:
|
||||||
|
return found
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _normalize_fetched_order(self, order: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
# 1) Ensure stopPrice exists for trigger orders
|
||||||
|
if order.get("stopPrice") is None:
|
||||||
|
raw = (
|
||||||
|
order.get("triggerPrice")
|
||||||
|
or order.get("trigger_price")
|
||||||
|
or order.get("stop_price")
|
||||||
|
or self._find_first_value(
|
||||||
|
order.get("info"),
|
||||||
|
{"stopPrice", "triggerPrice", "stop_price", "trigger_price"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sp = self._safe_float(raw)
|
||||||
|
if sp is not None:
|
||||||
|
order["stopPrice"] = sp
|
||||||
|
|
||||||
|
# 2) Fix type when we clearly have a market trigger (no limit price, but has stopPrice)
|
||||||
|
if (
|
||||||
|
order.get("type") in (None, "limit")
|
||||||
|
and order.get("price") is None
|
||||||
|
and order.get("stopPrice") is not None
|
||||||
|
):
|
||||||
|
order["type"] = "market"
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
def fetch_open_orders(
|
||||||
|
self,
|
||||||
|
pair: str | None = None,
|
||||||
|
since: int | None = None,
|
||||||
|
limit: int | None = None,
|
||||||
|
params: dict[str, Any] | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
params = self._filter_params_for_open_closed(params or {})
|
||||||
|
return self._api.fetch_open_orders(pair, since, limit, params)
|
||||||
|
|
||||||
|
def fetch_closed_orders(
|
||||||
|
self,
|
||||||
|
pair: str | None = None,
|
||||||
|
since: int | None = None,
|
||||||
|
limit: int | None = None,
|
||||||
|
params: dict[str, Any] | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
params = self._filter_params_for_open_closed(params or {})
|
||||||
|
return self._api.fetch_closed_orders(pair, since, limit, params)
|
||||||
|
|
||||||
|
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> dict[str, Any]:
|
||||||
|
params: dict[str, Any] = super()._get_stop_params(
|
||||||
|
side=side, ordertype=ordertype, stop_price=stop_price
|
||||||
|
)
|
||||||
|
|
||||||
|
# Force Kraken Futures naming
|
||||||
|
params.setdefault("triggerPrice", stop_price)
|
||||||
|
|
||||||
|
trigger_signal = self._get_trigger_signal()
|
||||||
|
if trigger_signal is not None:
|
||||||
|
params.setdefault("triggerSignal", trigger_signal)
|
||||||
|
|
||||||
|
if getattr(self, "trading_mode", None) == TradingMode.FUTURES:
|
||||||
|
params.setdefault("reduceOnly", True)
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
def _get_trigger_signal(self) -> str | None:
|
||||||
|
ex_conf = self._config.get("exchange")
|
||||||
|
if isinstance(ex_conf, dict):
|
||||||
|
v = ex_conf.get("triggerSignal") or ex_conf.get("trigger_signal")
|
||||||
|
if isinstance(v, str) and v.strip():
|
||||||
|
return v.strip()
|
||||||
|
return "mark"
|
||||||
|
|
||||||
|
def ohlcv_candle_limit(
|
||||||
|
self,
|
||||||
|
timeframe: str,
|
||||||
|
candle_type: CandleType,
|
||||||
|
since_ms: int | None = None,
|
||||||
|
) -> int:
|
||||||
|
limit = super().ohlcv_candle_limit(
|
||||||
|
timeframe,
|
||||||
|
candle_type=candle_type,
|
||||||
|
since_ms=since_ms,
|
||||||
|
)
|
||||||
|
return min(limit, 2000)
|
||||||
|
|
||||||
|
|
||||||
|
class Krakenfutures(KrakenFutures):
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user