diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 3ebfd809c..60a058952 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -9,7 +9,7 @@ from collections import deque from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple -from pandas import DataFrame, to_timedelta +from pandas import DataFrame, Timedelta, Timestamp, to_timedelta from freqtrade.configuration import TimeRange from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes, @@ -206,9 +206,11 @@ class DataProvider: existing_df, _ = self.__producer_pairs_df[producer_name][pair_key] # CHECK FOR MISSING CANDLES - timeframe_delta = to_timedelta(timeframe) # Convert the timeframe to a timedelta for pandas - local_last = existing_df.iloc[-1]['date'] # We want the last date from our copy - incoming_first = dataframe.iloc[0]['date'] # We want the first date from the incoming + # Convert the timeframe to a timedelta for pandas + timeframe_delta: Timedelta = to_timedelta(timeframe) + local_last: Timestamp = existing_df.iloc[-1]['date'] # We want the last date from our copy + # We want the first date from the incoming + incoming_first: Timestamp = dataframe.iloc[0]['date'] # Remove existing candles that are newer than the incoming first candle existing_df1 = existing_df[existing_df['date'] < incoming_first] @@ -221,7 +223,7 @@ class DataProvider: # we missed some candles between our data and the incoming # so return False and candle_difference. if candle_difference > 1: - return (False, candle_difference) + return (False, int(candle_difference)) if existing_df1.empty: appended_df = dataframe else: diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 7626198dc..6637663ff 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -308,7 +308,7 @@ class IDataHandler(ABC): timerange=timerange_startup, candle_type=candle_type ) - if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True): + if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data): return pairdf else: enddate = pairdf.iloc[-1]['date'] @@ -316,7 +316,7 @@ class IDataHandler(ABC): if timerange_startup: self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup) pairdf = trim_dataframe(pairdf, timerange_startup) - if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data): + if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True): return pairdf # incomplete candles should only be dropped if we didn't trim the end beforehand. diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 55bfbd232..d0598d8de 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -34,6 +34,7 @@ class Bybit(Exchange): "ohlcv_candle_limit": 200, "ohlcv_has_history": True, "mark_ohlcv_timeframe": "4h", + "funding_fee_timeframe": "8h", "stoploss_on_exchange": True, "stoploss_order_types": {"limit": "limit", "market": "market"}, } diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 6c7d7acfc..797d9fbd2 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -36,3 +36,34 @@ class Kucoin(Exchange): 'stop': 'loss' }) return params + + def create_order( + self, + *, + pair: str, + ordertype: str, + side: BuySell, + amount: float, + rate: float, + leverage: float, + reduceOnly: bool = False, + time_in_force: str = 'GTC', + ) -> Dict: + + res = super().create_order( + pair=pair, + ordertype=ordertype, + side=side, + amount=amount, + rate=rate, + leverage=leverage, + reduceOnly=reduceOnly, + time_in_force=time_in_force, + ) + # Kucoin returns only the order-id. + # ccxt returns status = 'closed' at the moment - which is information ccxt invented. + # Since we rely on status heavily, we must set it to 'open' here. + # ref: https://github.com/ccxt/ccxt/pull/16674, (https://github.com/ccxt/ccxt/pull/16553) + res['type'] = ordertype + res['status'] = 'open' + return res diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 07c58cbee..9da448c62 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -750,13 +750,15 @@ class FreqtradeBot(LoggingMixin): self.exchange.name, order['filled'], order['amount'], order['remaining'] ) - amount = safe_value_fallback(order, 'filled', 'amount') - enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + amount = safe_value_fallback(order, 'filled', 'amount', amount) + enter_limit_filled_price = safe_value_fallback( + order, 'average', 'price', enter_limit_filled_price) # in case of FOK the order may be filled immediately and fully elif order_status == 'closed': - amount = safe_value_fallback(order, 'filled', 'amount') - enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + amount = safe_value_fallback(order, 'filled', 'amount', amount) + enter_limit_filled_price = safe_value_fallback( + order, 'average', 'price', enter_limit_requested) # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py index 9520123ee..88c97989a 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py @@ -44,7 +44,7 @@ class SharpeHyperOptLossDaily(IHyperOptLoss): sum_daily = ( results.resample(resample_freq, on='close_date').agg( - {"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0) + {"profit_ratio_after_slippage": 'sum'}).reindex(t_index).fillna(0) ) total_profit = sum_daily["profit_ratio_after_slippage"] - risk_free_rate diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py index fac96664d..f5fe4590e 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py @@ -46,7 +46,7 @@ class SortinoHyperOptLossDaily(IHyperOptLoss): sum_daily = ( results.resample(resample_freq, on='close_date').agg( - {"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0) + {"profit_ratio_after_slippage": 'sum'}).reindex(t_index).fillna(0) ) total_profit = sum_daily["profit_ratio_after_slippage"] - minimum_acceptable_return diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index daeccb216..a112771d3 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -172,7 +172,7 @@ class Order(_DECL_BASE): def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]: resp = { - 'amount': self.amount, + 'amount': self.safe_amount, 'safe_price': self.safe_price, 'ft_order_side': self.ft_order_side, 'order_filled_timestamp': int(self.order_filled_date.replace( diff --git a/freqtrade/rpc/api_server/api_ws.py b/freqtrade/rpc/api_server/api_ws.py index 18714f15f..b253d66c2 100644 --- a/freqtrade/rpc/api_server/api_ws.py +++ b/freqtrade/rpc/api_server/api_ws.py @@ -90,7 +90,7 @@ async def _process_consumer_request( elif type == RPCRequestType.ANALYZED_DF: # Limit the amount of candles per dataframe to 'limit' or 1500 - limit = min(data.get('limit', 1500), 1500) if data else None + limit = int(min(data.get('limit', 1500), 1500)) if data else None pair = data.get('pair', None) if data else None # For every pair in the generator, send a separate message diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index e0c79d52a..c6b1dcc5a 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -437,6 +437,7 @@ def test_dp__add_external_df(default_conf_usdt): # Add the same dataframe again - dataframe size shall not change. res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is True + assert isinstance(res[1], int) assert res[1] == 0 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) assert len(df) == 24 @@ -446,6 +447,7 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df2, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is True + assert isinstance(res[1], int) assert res[1] == 0 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) assert len(df) == 48 @@ -455,6 +457,7 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df3, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is True + assert isinstance(res[1], int) assert res[1] == 0 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) # New length = 48 + 12 (since we have a 12 hour offset). @@ -478,6 +481,7 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is False # 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00 + assert isinstance(res[1], int) assert res[1] == 36 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) # New length = 61 + 1 @@ -488,4 +492,5 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is False # 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00 + assert isinstance(res[1], int) assert res[1] == 0 diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 2ca92799f..c4757288d 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -49,8 +49,8 @@ EXCHANGES = { "orderListId": -1, "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", "transactTime": 1674493798550, - "price": "15.00000000", - "origQty": "1.00000000", + "price": "15.50000000", + "origQty": "1.10000000", "executedQty": "0.00000000", "cummulativeQuoteQty": "0.00000000", "status": "NEW", @@ -74,8 +74,8 @@ EXCHANGES = { "orderListId": -1, "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", "transactTime": 1674493798550, - "price": "15.00000000", - "origQty": "1.00000000", + "price": "15.50000000", + "origQty": "1.10000000", "executedQty": "0.00000000", "cummulativeQuoteQty": "0.00000000", "status": "NEW", @@ -106,12 +106,12 @@ EXCHANGES = { {'id': '63d6742d0adc5570001d2bbf7'}, # create order { 'id': '63d6742d0adc5570001d2bbf7', - 'symbol': 'NAKA-USDT', + 'symbol': 'SOL-USDT', 'opType': 'DEAL', 'type': 'limit', 'side': 'buy', - 'price': '30', - 'size': '0.1', + 'price': '15.5', + 'size': '1.1', 'funds': '0', 'dealFunds': '0.032626', 'dealSize': '0.1', @@ -168,6 +168,23 @@ EXCHANGES = { 'futures': True, 'leverage_tiers_public': True, 'leverage_in_spot_market': True, + 'sample_order': [ + { + "orderId": "1274754916287346280", + "orderLinkId": "1666798627015730", + "symbol": "SOLUSDT", + "createTime": "1674493798550", + "orderPrice": "15.5", + "orderQty": "1.1", + "orderType": "LIMIT", + "side": "BUY", + "status": "NEW", + "timeInForce": "GTC", + "accountId": "5555555", + "execQty": "0", + "orderCategory": "0" + } + ] }, 'huobi': { 'pair': 'ETH/BTC', @@ -306,13 +323,19 @@ class TestCCXTExchange(): po = exch._api.parse_order(order) assert isinstance(po['id'], str) assert po['id'] is not None - if len(order.keys()) > 1: - assert po['timestamp'] == 1674493798550 - assert isinstance(po['datetime'], str) - assert isinstance(po['timestamp'], int) - assert isinstance(po['price'], float) - assert isinstance(po['amount'], float) - assert isinstance(po['status'], str) + if len(order.keys()) < 5: + # Kucoin case + assert po['status'] == 'closed' + continue + assert po['timestamp'] == 1674493798550 + assert isinstance(po['datetime'], str) + assert isinstance(po['timestamp'], int) + assert isinstance(po['price'], float) + assert po['price'] == 15.5 + assert po['symbol'] == 'SOL/USDT' + assert isinstance(po['amount'], float) + assert po['amount'] == 1.1 + assert isinstance(po['status'], str) else: pytest.skip(f"No sample order available for exchange {exchange_name}")