Merge branch 'feat/short' into leverage-tiers
This commit is contained in:
commit
88a8ff2f4e
@ -8,8 +8,10 @@ class CandleType(str, Enum):
|
|||||||
MARK = "mark"
|
MARK = "mark"
|
||||||
INDEX = "index"
|
INDEX = "index"
|
||||||
PREMIUMINDEX = "premiumIndex"
|
PREMIUMINDEX = "premiumIndex"
|
||||||
# TODO-lev: not sure this belongs here, as the datatype is really different
|
|
||||||
|
# TODO: Could take up less memory if these weren't a CandleType
|
||||||
FUNDING_RATE = "funding_rate"
|
FUNDING_RATE = "funding_rate"
|
||||||
|
# BORROW_RATE = "borrow_rate" # * unimplemented
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string(value: str) -> 'CandleType':
|
def from_string(value: str) -> 'CandleType':
|
||||||
|
@ -10,16 +10,19 @@ class RPCMessageType(Enum):
|
|||||||
BUY_FILL = 'buy_fill'
|
BUY_FILL = 'buy_fill'
|
||||||
BUY_CANCEL = 'buy_cancel'
|
BUY_CANCEL = 'buy_cancel'
|
||||||
|
|
||||||
SELL = 'sell'
|
|
||||||
SELL_FILL = 'sell_fill'
|
|
||||||
SELL_CANCEL = 'sell_cancel'
|
|
||||||
PROTECTION_TRIGGER = 'protection_trigger'
|
|
||||||
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
|
||||||
|
|
||||||
SHORT = 'short'
|
SHORT = 'short'
|
||||||
SHORT_FILL = 'short_fill'
|
SHORT_FILL = 'short_fill'
|
||||||
SHORT_CANCEL = 'short_cancel'
|
SHORT_CANCEL = 'short_cancel'
|
||||||
|
|
||||||
|
# TODO: The below messagetypes should be renamed to "exit"!
|
||||||
|
# Careful - has an impact on webhooks, therefore needs proper communication
|
||||||
|
SELL = 'sell'
|
||||||
|
SELL_FILL = 'sell_fill'
|
||||||
|
SELL_CANCEL = 'sell_cancel'
|
||||||
|
|
||||||
|
PROTECTION_TRIGGER = 'protection_trigger'
|
||||||
|
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
@ -503,7 +503,7 @@ class Exchange:
|
|||||||
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
|
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
|
||||||
if self.markets and pair not in self.markets:
|
if self.markets and pair not in self.markets:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Pair {pair} is not available on {self.name}. '
|
f'Pair {pair} is not available on {self.name} {self.trading_mode.value}. '
|
||||||
f'Please remove {pair} from your whitelist.')
|
f'Please remove {pair} from your whitelist.')
|
||||||
|
|
||||||
# From ccxt Documentation:
|
# From ccxt Documentation:
|
||||||
@ -1533,7 +1533,6 @@ class Exchange:
|
|||||||
:return: Dict of [{(pair, timeframe): Dataframe}]
|
:return: Dict of [{(pair, timeframe): Dataframe}]
|
||||||
"""
|
"""
|
||||||
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
||||||
# TODO-lev: maybe depend this on candle type?
|
|
||||||
drop_incomplete = self._ohlcv_partial_candle if drop_incomplete is None else drop_incomplete
|
drop_incomplete = self._ohlcv_partial_candle if drop_incomplete is None else drop_incomplete
|
||||||
input_coroutines = []
|
input_coroutines = []
|
||||||
cached_pairs = []
|
cached_pairs = []
|
||||||
|
@ -740,6 +740,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# 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':
|
||||||
|
# TODO-lev: Evaluate this. Why is setting stake_amount here necessary?
|
||||||
|
# it should never change in theory - and in case of leveraged orders,
|
||||||
|
# may be the leveraged amount.
|
||||||
stake_amount = order['cost']
|
stake_amount = order['cost']
|
||||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||||
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||||
@ -1288,6 +1291,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# * Check edge cases, we don't want to make leverage > 1.0 if we don't have to
|
# * Check edge cases, we don't want to make leverage > 1.0 if we don't have to
|
||||||
# * (for leverage modes which aren't isolated futures)
|
# * (for leverage modes which aren't isolated futures)
|
||||||
|
|
||||||
|
# TODO-lev: The below calculation needs to include leverage ...
|
||||||
trade.stake_amount = trade.amount * trade.open_rate
|
trade.stake_amount = trade.amount * trade.open_rate
|
||||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||||
|
|
||||||
@ -1736,7 +1740,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
|
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
|
||||||
|
|
||||||
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
|
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
|
||||||
# TODO-lev: leverage?
|
# * Leverage could be a cause for this warning, leverage hasn't been thoroughly tested
|
||||||
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
|
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
|
||||||
raise DependencyException("Half bought? Amounts don't match")
|
raise DependencyException("Half bought? Amounts don't match")
|
||||||
|
|
||||||
|
@ -538,7 +538,6 @@ class Backtesting:
|
|||||||
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime()
|
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime()
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
# TODO-lev: liquidation price?
|
|
||||||
trade.funding_fees = self.exchange.calculate_funding_fees(
|
trade.funding_fees = self.exchange.calculate_funding_fees(
|
||||||
self.futures_data[trade.pair],
|
self.futures_data[trade.pair],
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
|
@ -15,7 +15,7 @@ import talib.abstract as ta
|
|||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
|
||||||
|
|
||||||
# TODO-lev: Create a meaningfull short strategy (not just revresed signs).
|
# TODO: Create a meaningfull short strategy (not just revresed signs).
|
||||||
# This class is a sample. Feel free to customize it.
|
# This class is a sample. Feel free to customize it.
|
||||||
class SampleShortStrategy(IStrategy):
|
class SampleShortStrategy(IStrategy):
|
||||||
"""
|
"""
|
||||||
|
@ -697,7 +697,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
|||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
backtesting.required_startup = 0
|
backtesting.required_startup = 0
|
||||||
if data.leverage > 1.0:
|
if data.leverage > 1.0:
|
||||||
# TODO-lev: Should we initialize this properly??
|
# TODO: Should we initialize this properly??
|
||||||
backtesting._can_short = True
|
backtesting._can_short = True
|
||||||
backtesting.strategy.advise_entry = lambda a, m: frame
|
backtesting.strategy.advise_entry = lambda a, m: frame
|
||||||
backtesting.strategy.advise_exit = lambda a, m: frame
|
backtesting.strategy.advise_exit = lambda a, m: frame
|
||||||
|
@ -71,7 +71,7 @@ class StrategyTestV3(IStrategy):
|
|||||||
protection_enabled = BooleanParameter(default=True)
|
protection_enabled = BooleanParameter(default=True)
|
||||||
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
||||||
|
|
||||||
# TODO-lev: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
|
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
|
||||||
# @property
|
# @property
|
||||||
# def protections(self):
|
# def protections(self):
|
||||||
# prot = []
|
# prot = []
|
||||||
|
@ -522,13 +522,11 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker,
|
|||||||
assert len(trades) == 4
|
assert len(trades) == 4
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('is_short, open_rate', [
|
@pytest.mark.parametrize('is_short', [False, True])
|
||||||
(False, 2.0),
|
|
||||||
(True, 2.02)
|
|
||||||
])
|
|
||||||
def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, limit_order_open,
|
def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, limit_order_open,
|
||||||
is_short, open_rate, fee, mocker, caplog
|
is_short, fee, mocker, caplog
|
||||||
) -> None:
|
) -> None:
|
||||||
|
ticker_side = 'ask' if is_short else 'bid'
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -554,8 +552,8 @@ def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, lim
|
|||||||
assert trade.is_open
|
assert trade.is_open
|
||||||
assert trade.open_date is not None
|
assert trade.open_date is not None
|
||||||
assert trade.exchange == 'binance'
|
assert trade.exchange == 'binance'
|
||||||
assert trade.open_rate == open_rate # TODO-lev: I think? That's what the ticker ask price is
|
assert trade.open_rate == ticker_usdt.return_value[ticker_side]
|
||||||
assert isclose(trade.amount, 60 / open_rate)
|
assert isclose(trade.amount, 60 / ticker_usdt.return_value[ticker_side])
|
||||||
|
|
||||||
assert log_has(
|
assert log_has(
|
||||||
f'{"Short" if is_short else "Long"} signal found: about create a new trade for ETH/USDT '
|
f'{"Short" if is_short else "Long"} signal found: about create a new trade for ETH/USDT '
|
||||||
@ -3275,9 +3273,9 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
|
|||||||
assert rpc_mock.call_count == 3
|
assert rpc_mock.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
# TODO-lev: add short, RPC short, short fill
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf_usdt, ticker_usdt, fee,
|
def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
|
||||||
mocker) -> None:
|
default_conf_usdt, ticker_usdt, fee, mocker, is_short) -> None:
|
||||||
default_conf_usdt['exchange']['name'] = 'binance'
|
default_conf_usdt['exchange']['name'] = 'binance'
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -3301,7 +3299,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf_usdt
|
|||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade, enter_long=not is_short, enter_short=is_short)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
@ -3315,7 +3313,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf_usdt
|
|||||||
assert trade.stoploss_order_id == '123'
|
assert trade.stoploss_order_id == '123'
|
||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
|
|
||||||
# Assuming stoploss on exchnage is hit
|
# Assuming stoploss on exchange is hit
|
||||||
# stoploss_order_id should become None
|
# stoploss_order_id should become None
|
||||||
# and trade should be sold at the price of stoploss
|
# and trade should be sold at the price of stoploss
|
||||||
stoploss_executed = MagicMock(return_value={
|
stoploss_executed = MagicMock(return_value={
|
||||||
@ -3343,19 +3341,24 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf_usdt
|
|||||||
assert trade.is_open is False
|
assert trade.is_open is False
|
||||||
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
|
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
assert rpc_mock.call_count == 3
|
assert rpc_mock.call_count == 3
|
||||||
|
if is_short:
|
||||||
|
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.SHORT
|
||||||
|
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.SHORT_FILL
|
||||||
|
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
|
||||||
|
|
||||||
|
else:
|
||||||
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY
|
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY
|
||||||
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL
|
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL
|
||||||
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
|
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"is_short,amount,open_rate,current_rate,limit,profit_amount,profit_ratio,profit_or_loss", [
|
"is_short,amount,current_rate,limit,profit_amount,profit_ratio,profit_or_loss", [
|
||||||
(False, 30, 2.0, 2.3, 2.2, 5.685, 0.09451372, 'profit'),
|
(False, 30, 2.3, 2.2, 5.685, 0.09451372, 'profit'),
|
||||||
# TODO-lev: Should the current rate be 2.2 for shorts?
|
(True, 29.70297029, 2.2, 2.3, -8.63762376, -0.1443212, 'loss'),
|
||||||
(True, 29.70297029, 2.02, 2.2, 2.3, -8.63762376, -0.1443212, 'loss'),
|
|
||||||
])
|
])
|
||||||
def test_execute_trade_exit_market_order(
|
def test_execute_trade_exit_market_order(
|
||||||
default_conf_usdt, ticker_usdt, fee, is_short, current_rate, amount, open_rate,
|
default_conf_usdt, ticker_usdt, fee, is_short, current_rate, amount,
|
||||||
limit, profit_amount, profit_ratio, profit_or_loss, ticker_usdt_sell_up, mocker
|
limit, profit_amount, profit_ratio, profit_or_loss, ticker_usdt_sell_up, mocker
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@ -3375,6 +3378,7 @@ def test_execute_trade_exit_market_order(
|
|||||||
long: (65.835/60.15) - 1 = 0.0945137157107232
|
long: (65.835/60.15) - 1 = 0.0945137157107232
|
||||||
short: 1 - (68.48762376237624/59.85) = -0.1443211990371971
|
short: 1 - (68.48762376237624/59.85) = -0.1443211990371971
|
||||||
"""
|
"""
|
||||||
|
open_rate = ticker_usdt.return_value['ask' if is_short else 'bid']
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -4241,14 +4245,13 @@ def test_apply_fee_conditional(default_conf_usdt, fee, mocker,
|
|||||||
(0.1, False),
|
(0.1, False),
|
||||||
(100, True),
|
(100, True),
|
||||||
])
|
])
|
||||||
@pytest.mark.parametrize('is_short, open_rate', [
|
@pytest.mark.parametrize('is_short', [False, True])
|
||||||
(False, 2.0),
|
|
||||||
(True, 2.02),
|
|
||||||
])
|
|
||||||
def test_order_book_depth_of_market(
|
def test_order_book_depth_of_market(
|
||||||
default_conf_usdt, ticker_usdt, limit_order, limit_order_open,
|
default_conf_usdt, ticker_usdt, limit_order_open,
|
||||||
fee, mocker, order_book_l2, delta, is_high_delta, is_short, open_rate
|
fee, mocker, order_book_l2, delta, is_high_delta, is_short
|
||||||
):
|
):
|
||||||
|
ticker_side = 'ask' if is_short else 'bid'
|
||||||
|
|
||||||
default_conf_usdt['bid_strategy']['check_depth_of_market']['enabled'] = True
|
default_conf_usdt['bid_strategy']['check_depth_of_market']['enabled'] = True
|
||||||
default_conf_usdt['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = delta
|
default_conf_usdt['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = delta
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
@ -4283,7 +4286,7 @@ def test_order_book_depth_of_market(
|
|||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_order_open[enter_side(is_short)])
|
trade.update(limit_order_open[enter_side(is_short)])
|
||||||
|
|
||||||
assert trade.open_rate == open_rate # TODO-lev: double check
|
assert trade.open_rate == ticker_usdt.return_value[ticker_side]
|
||||||
assert whitelist == default_conf_usdt['exchange']['pair_whitelist']
|
assert whitelist == default_conf_usdt['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user