freqtrade_origin/freqtrade/exchange/kraken.py

192 lines
7.1 KiB
Python
Raw Normal View History

2024-05-12 14:58:33 +00:00
"""Kraken exchange subclass"""
2019-02-17 03:01:17 +00:00
import logging
from datetime import datetime
2021-09-19 23:02:09 +00:00
from typing import Any, Dict, List, Optional, Tuple
2019-02-17 03:01:17 +00:00
import ccxt
from pandas import DataFrame
2022-05-07 08:56:13 +00:00
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
2024-01-17 18:43:53 +00:00
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
2020-05-18 12:20:51 +00:00
from freqtrade.exchange.common import retrier
2024-09-04 05:15:17 +00:00
from freqtrade.exchange.exchange_types import CcxtBalances, FtHas, Tickers
2020-09-28 17:39:41 +00:00
2019-02-17 03:01:17 +00:00
logger = logging.getLogger(__name__)
class Kraken(Exchange):
2019-02-17 14:54:22 +00:00
_params: Dict = {"trading_agreement": "agree"}
2024-09-04 05:15:17 +00:00
_ft_has: FtHas = {
2020-01-19 13:08:47 +00:00
"stoploss_on_exchange": True,
2024-01-17 18:44:09 +00:00
"stop_price_param": "stopLossPrice",
"stop_price_prop": "stopLossPrice",
"stoploss_order_types": {"limit": "limit", "market": "market"},
"order_time_in_force": ["GTC", "IOC", "PO"],
2020-12-20 10:44:50 +00:00
"ohlcv_candle_limit": 720,
"ohlcv_has_history": False,
2019-08-14 17:22:52 +00:00
"trades_pagination": "id",
"trades_pagination_arg": "since",
"trades_pagination_overlap": False,
2024-06-20 16:24:43 +00:00
"trades_has_history": True,
"mark_ohlcv_timeframe": "4h",
2019-08-14 17:22:52 +00:00
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
2021-09-19 23:02:09 +00:00
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS)
2021-09-19 23:02:09 +00:00
]
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
"""
Check if the market symbol is tradable by Freqtrade.
Default checks + check if pair is darkpool pair.
"""
parent_check = super().market_is_tradable(market)
2024-05-12 14:58:33 +00:00
return parent_check and market.get("darkpool", False) is False
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
# Only fetch tickers for current stake currency
# Otherwise the request for kraken becomes too large.
2024-05-12 14:58:33 +00:00
symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]]))
return super().get_tickers(symbols=symbols, cached=cached)
@retrier
def get_balances(self) -> CcxtBalances:
2024-05-12 14:58:33 +00:00
if self._config["dry_run"]:
return {}
try:
balances = self._api.fetch_balance()
# Remove additional info from ccxt results
balances.pop("info", None)
balances.pop("free", None)
balances.pop("total", None)
balances.pop("used", None)
orders = self._api.fetch_open_orders()
2024-05-12 14:58:33 +00:00
order_list = [
(
x["symbol"].split("/")[0 if x["side"] == "sell" else 1],
x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"],
# Don't remove the below comment, this can be important for debugging
# x["side"], x["amount"],
)
for x in orders
if x["remaining"] is not None and (x["side"] == "sell" or x["price"] is not None)
2024-05-12 14:58:33 +00:00
]
for bal in balances:
if not isinstance(balances[bal], dict):
continue
2024-05-12 14:58:33 +00:00
balances[bal]["used"] = sum(order[1] for order in order_list if order[0] == bal)
balances[bal]["free"] = balances[bal]["total"] - balances[bal]["used"]
return balances
2020-06-28 09:17:06 +00:00
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
2024-05-12 14:58:33 +00:00
f"Could not get balance due to {e.__class__.__name__}. Message: {e}"
) from e
2020-01-19 13:08:47 +00:00
except ccxt.BaseError as e:
raise OperationalException(e) from e
2021-09-19 23:02:09 +00:00
def _set_leverage(
self,
leverage: float,
pair: Optional[str] = None,
2023-01-08 10:04:47 +00:00
accept_fail: bool = False,
2021-09-19 23:02:09 +00:00
):
"""
2021-11-09 18:22:29 +00:00
Kraken set's the leverage as an option in the order object, so we need to
add it to params
2021-09-19 23:02:09 +00:00
"""
return
2022-02-02 04:23:05 +00:00
def _get_params(
self,
2022-05-07 08:56:13 +00:00
side: BuySell,
2022-02-02 04:23:05 +00:00
ordertype: str,
leverage: float,
reduceOnly: bool,
2024-05-12 14:58:33 +00:00
time_in_force: str = "GTC",
2022-02-02 04:23:05 +00:00
) -> Dict:
2022-03-23 05:49:07 +00:00
params = super()._get_params(
2022-05-07 08:56:13 +00:00
side=side,
2022-03-23 05:49:07 +00:00
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
2021-09-19 23:02:09 +00:00
if leverage > 1.0:
2024-05-12 14:58:33 +00:00
params["leverage"] = round(leverage)
if time_in_force == "PO":
params.pop("timeInForce", None)
params["postOnly"] = True
2021-09-19 23:02:09 +00:00
return params
def calculate_funding_fees(
self,
df: DataFrame,
amount: float,
is_short: bool,
open_date: datetime,
2023-10-07 13:09:44 +00:00
close_date: datetime,
2024-05-12 14:58:33 +00:00
time_in_ratio: Optional[float] = None,
) -> float:
"""
2021-11-09 07:00:57 +00:00
# ! This method will always error when run by Freqtrade because time_in_ratio is never
# ! passed to _get_funding_fee. For kraken futures to work in dry run and backtesting
# ! functionality must be added that passes the parameter time_in_ratio to
# ! _get_funding_fee when using Kraken
calculates the sum of all funding fees that occurred for a pair during a futures trade
:param df: Dataframe containing combined funding and mark rates
as `open_fund` and `open_mark`.
:param amount: The quantity of the trade
:param is_short: trade direction
:param open_date: The date and time that the trade started
:param close_date: The date and time that the trade ended
:param time_in_ratio: Not used by most exchange classes
"""
if not time_in_ratio:
raise OperationalException(
2024-05-12 14:58:33 +00:00
f"time_in_ratio is required for {self.name}._get_funding_fee"
)
fees: float = 0
if not df.empty:
2024-05-12 14:58:33 +00:00
df = df[(df["date"] >= open_date) & (df["date"] <= close_date)]
fees = sum(df["open_fund"] * df["open_mark"] * amount * time_in_ratio)
2022-01-22 10:04:58 +00:00
return fees if is_short else -fees
def _get_trade_pagination_next_value(self, trades: List[Dict]):
"""
2024-01-21 14:55:34 +00:00
Extract pagination id for the next "from_id" value
Applies only to fetch_trade_history by id.
"""
2024-01-21 14:55:34 +00:00
if len(trades) > 0:
2024-05-12 14:58:33 +00:00
if isinstance(trades[-1].get("info"), list) and len(trades[-1].get("info", [])) > 7:
2024-01-21 14:55:34 +00:00
# Trade response's "last" value.
2024-05-12 14:58:33 +00:00
return trades[-1].get("info", [])[-1]
2024-01-21 14:55:34 +00:00
# Fall back to timestamp if info is somehow empty.
2024-05-12 14:58:33 +00:00
return trades[-1].get("timestamp")
2024-01-21 14:55:34 +00:00
return None
2024-01-21 13:08:35 +00:00
def _valid_trade_pagination_id(self, pair: str, from_id: str) -> bool:
"""
Verify trade-pagination id is valid.
Workaround for odd Kraken issue where ID is sometimes wrong.
"""
# Regular id's are in timestamp format 1705443695120072285
# If the id is smaller than 19 characters, it's not a valid timestamp.
if len(from_id) >= 19:
return True
logger.debug(f"{pair} - trade-pagination id is not valid. Fallback to timestamp.")
2024-01-21 13:08:35 +00:00
return False