Merge branch 'develop' into feat/cancel_order

This commit is contained in:
Matthias 2023-02-01 07:06:17 +00:00
commit c1a34396d0
11 changed files with 93 additions and 29 deletions

View File

@ -9,7 +9,7 @@ from collections import deque
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple 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.configuration import TimeRange
from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes, 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] existing_df, _ = self.__producer_pairs_df[producer_name][pair_key]
# CHECK FOR MISSING CANDLES # CHECK FOR MISSING CANDLES
timeframe_delta = to_timedelta(timeframe) # Convert the timeframe to a timedelta for pandas # Convert the timeframe to a timedelta for pandas
local_last = existing_df.iloc[-1]['date'] # We want the last date from our copy timeframe_delta: Timedelta = to_timedelta(timeframe)
incoming_first = dataframe.iloc[0]['date'] # We want the first date from the incoming 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 # Remove existing candles that are newer than the incoming first candle
existing_df1 = existing_df[existing_df['date'] < incoming_first] 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 # we missed some candles between our data and the incoming
# so return False and candle_difference. # so return False and candle_difference.
if candle_difference > 1: if candle_difference > 1:
return (False, candle_difference) return (False, int(candle_difference))
if existing_df1.empty: if existing_df1.empty:
appended_df = dataframe appended_df = dataframe
else: else:

View File

@ -308,7 +308,7 @@ class IDataHandler(ABC):
timerange=timerange_startup, timerange=timerange_startup,
candle_type=candle_type 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 return pairdf
else: else:
enddate = pairdf.iloc[-1]['date'] enddate = pairdf.iloc[-1]['date']
@ -316,7 +316,7 @@ class IDataHandler(ABC):
if timerange_startup: if timerange_startup:
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup) self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
pairdf = trim_dataframe(pairdf, 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 return pairdf
# incomplete candles should only be dropped if we didn't trim the end beforehand. # incomplete candles should only be dropped if we didn't trim the end beforehand.

View File

@ -34,6 +34,7 @@ class Bybit(Exchange):
"ohlcv_candle_limit": 200, "ohlcv_candle_limit": 200,
"ohlcv_has_history": True, "ohlcv_has_history": True,
"mark_ohlcv_timeframe": "4h", "mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit", "market": "market"}, "stoploss_order_types": {"limit": "limit", "market": "market"},
} }

View File

@ -36,3 +36,34 @@ class Kucoin(Exchange):
'stop': 'loss' 'stop': 'loss'
}) })
return params 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

View File

@ -750,13 +750,15 @@ class FreqtradeBot(LoggingMixin):
self.exchange.name, order['filled'], order['amount'], self.exchange.name, order['filled'], order['amount'],
order['remaining'] order['remaining']
) )
amount = safe_value_fallback(order, 'filled', 'amount') amount = safe_value_fallback(order, 'filled', 'amount', amount)
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') 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 # in case of FOK the order may be filled immediately and fully
elif order_status == 'closed': elif order_status == 'closed':
amount = safe_value_fallback(order, 'filled', 'amount') amount = safe_value_fallback(order, 'filled', 'amount', amount)
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') 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 is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')

View File

@ -44,7 +44,7 @@ class SharpeHyperOptLossDaily(IHyperOptLoss):
sum_daily = ( sum_daily = (
results.resample(resample_freq, on='close_date').agg( 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 total_profit = sum_daily["profit_ratio_after_slippage"] - risk_free_rate

View File

@ -46,7 +46,7 @@ class SortinoHyperOptLossDaily(IHyperOptLoss):
sum_daily = ( sum_daily = (
results.resample(resample_freq, on='close_date').agg( 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 total_profit = sum_daily["profit_ratio_after_slippage"] - minimum_acceptable_return

View File

@ -172,7 +172,7 @@ class Order(_DECL_BASE):
def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]: def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
resp = { resp = {
'amount': self.amount, 'amount': self.safe_amount,
'safe_price': self.safe_price, 'safe_price': self.safe_price,
'ft_order_side': self.ft_order_side, 'ft_order_side': self.ft_order_side,
'order_filled_timestamp': int(self.order_filled_date.replace( 'order_filled_timestamp': int(self.order_filled_date.replace(

View File

@ -90,7 +90,7 @@ async def _process_consumer_request(
elif type == RPCRequestType.ANALYZED_DF: elif type == RPCRequestType.ANALYZED_DF:
# Limit the amount of candles per dataframe to 'limit' or 1500 # 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 pair = data.get('pair', None) if data else None
# For every pair in the generator, send a separate message # For every pair in the generator, send a separate message

View File

@ -437,6 +437,7 @@ def test_dp__add_external_df(default_conf_usdt):
# Add the same dataframe again - dataframe size shall not change. # Add the same dataframe again - dataframe size shall not change.
res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT) res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is True assert res[0] is True
assert isinstance(res[1], int)
assert res[1] == 0 assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
assert len(df) == 24 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) res = dp._add_external_df('ETH/USDT', df2, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is True assert res[0] is True
assert isinstance(res[1], int)
assert res[1] == 0 assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
assert len(df) == 48 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) res = dp._add_external_df('ETH/USDT', df3, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is True assert res[0] is True
assert isinstance(res[1], int)
assert res[1] == 0 assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
# New length = 48 + 12 (since we have a 12 hour offset). # 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) res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is False 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 # 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 assert res[1] == 36
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
# New length = 61 + 1 # 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) res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is False 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 # 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 assert res[1] == 0

View File

@ -49,8 +49,8 @@ EXCHANGES = {
"orderListId": -1, "orderListId": -1,
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
"transactTime": 1674493798550, "transactTime": 1674493798550,
"price": "15.00000000", "price": "15.50000000",
"origQty": "1.00000000", "origQty": "1.10000000",
"executedQty": "0.00000000", "executedQty": "0.00000000",
"cummulativeQuoteQty": "0.00000000", "cummulativeQuoteQty": "0.00000000",
"status": "NEW", "status": "NEW",
@ -74,8 +74,8 @@ EXCHANGES = {
"orderListId": -1, "orderListId": -1,
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
"transactTime": 1674493798550, "transactTime": 1674493798550,
"price": "15.00000000", "price": "15.50000000",
"origQty": "1.00000000", "origQty": "1.10000000",
"executedQty": "0.00000000", "executedQty": "0.00000000",
"cummulativeQuoteQty": "0.00000000", "cummulativeQuoteQty": "0.00000000",
"status": "NEW", "status": "NEW",
@ -106,12 +106,12 @@ EXCHANGES = {
{'id': '63d6742d0adc5570001d2bbf7'}, # create order {'id': '63d6742d0adc5570001d2bbf7'}, # create order
{ {
'id': '63d6742d0adc5570001d2bbf7', 'id': '63d6742d0adc5570001d2bbf7',
'symbol': 'NAKA-USDT', 'symbol': 'SOL-USDT',
'opType': 'DEAL', 'opType': 'DEAL',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'price': '30', 'price': '15.5',
'size': '0.1', 'size': '1.1',
'funds': '0', 'funds': '0',
'dealFunds': '0.032626', 'dealFunds': '0.032626',
'dealSize': '0.1', 'dealSize': '0.1',
@ -168,6 +168,23 @@ EXCHANGES = {
'futures': True, 'futures': True,
'leverage_tiers_public': True, 'leverage_tiers_public': True,
'leverage_in_spot_market': 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': { 'huobi': {
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
@ -306,12 +323,18 @@ class TestCCXTExchange():
po = exch._api.parse_order(order) po = exch._api.parse_order(order)
assert isinstance(po['id'], str) assert isinstance(po['id'], str)
assert po['id'] is not None assert po['id'] is not None
if len(order.keys()) > 1: if len(order.keys()) < 5:
# Kucoin case
assert po['status'] == 'closed'
continue
assert po['timestamp'] == 1674493798550 assert po['timestamp'] == 1674493798550
assert isinstance(po['datetime'], str) assert isinstance(po['datetime'], str)
assert isinstance(po['timestamp'], int) assert isinstance(po['timestamp'], int)
assert isinstance(po['price'], float) assert isinstance(po['price'], float)
assert po['price'] == 15.5
assert po['symbol'] == 'SOL/USDT'
assert isinstance(po['amount'], float) assert isinstance(po['amount'], float)
assert po['amount'] == 1.1
assert isinstance(po['status'], str) assert isinstance(po['status'], str)
else: else:
pytest.skip(f"No sample order available for exchange {exchange_name}") pytest.skip(f"No sample order available for exchange {exchange_name}")