Stake_amount only adjusted on partially filled trades if amount filled makes leverage <= 1. Fulfilled some other TODOs in freqtradebot

This commit is contained in:
Sam Germain 2021-07-26 18:36:02 -06:00
parent 6bbe63eb62
commit 7ac73999f6
9 changed files with 60 additions and 51 deletions

View File

@ -38,7 +38,8 @@ class Binance(Exchange):
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict:
def stoploss(self, pair: str, amount: float,
stop_price: float, order_types: Dict, side: str) -> Dict:
"""
creates a stoploss limit order.
this stoploss-limit is binance-specific.

View File

@ -777,14 +777,15 @@ class Exchange:
):
raise OperationalException(f"Leverage is not available on {self.name} using freqtrade")
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
raise OperationalException(f"stoploss is not implemented for {self.name}.")
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict:
def stoploss(self, pair: str, amount: float,
stop_price: float, order_types: Dict, side: str) -> Dict:
"""
creates a stoploss order.
The precise ordertype is determined by the order_types dict or exchange default.

View File

@ -43,7 +43,8 @@ class Ftx(Exchange):
return order['type'] == 'stop' and stop_loss > float(order['price'])
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict:
def stoploss(self, pair: str, amount: float,
stop_price: float, order_types: Dict, side: str) -> Dict:
"""
Creates a stoploss order.
depending on order_types.stoploss configuration, uses 'market' or limit order.

View File

@ -81,7 +81,8 @@ class Kraken(Exchange):
and stop_loss > float(order['price']))
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict:
def stoploss(self, pair: str, amount: float,
stop_price: float, order_types: Dict, side: str) -> Dict:
"""
Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken.

View File

@ -5,7 +5,7 @@ import copy
import logging
import traceback
from datetime import datetime, timezone
from math import isclose
from math import ceil, isclose
from threading import Lock
from typing import Any, Dict, List, Optional
@ -16,7 +16,7 @@ from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge
from freqtrade.enums import InterestMode, RPCMessageType, SellType, State
from freqtrade.enums import RPCMessageType, SellType, State
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
@ -101,9 +101,8 @@ class FreqtradeBot(LoggingMixin):
initial_state = self.config.get('initial_state')
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
# Protect sell-logic from forcesell and vice versa
# TODO-mg: update to _close_lock
self._sell_lock = Lock()
# Protect exit-logic from forcesell and vice versa
self._exit_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
# Start calculating maintenance margin if on cross margin
@ -177,16 +176,15 @@ class FreqtradeBot(LoggingMixin):
self.strategy.analyze(self.active_pair_whitelist)
# TODO-mg: update to _close_lock
with self._sell_lock:
with self._exit_lock:
# Check and handle any timed out open orders
self.check_handle_timedout()
# Protect from collisions with forcesell(#TODO-mg: update to forceclose).
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
# while closing is in process, since telegram messages arrive in an different thread.
# TODO-mg: update to _close_lock
with self._sell_lock:
with self._exit_lock:
trades = Trade.get_open_trades()
# First process current opened trades (positions)
self.exit_positions(trades)
@ -263,7 +261,6 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Updating {len(orders)} open orders.")
for order in orders:
try:
# TODO-mg: How to consider borrow orders?
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
order.ft_order_side == 'stoploss')
@ -412,8 +409,6 @@ class FreqtradeBot(LoggingMixin):
def create_trade(self, pair: str) -> bool:
"""
# TODO-mg: Just make this function work for shorting and leverage, my todo notes in
# TODO-mg: it aren't very clear
Check the implemented trading strategy for enter signals.
If the pair triggers the enter signal a new trade record gets created
@ -442,8 +437,8 @@ class FreqtradeBot(LoggingMixin):
logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.")
return False
long_lev = 1.0 if True else 1.0 # Replace with self.strategy.get_leverage
short_lev = 1.0 if True else 1.0 # Replace with self.strategy.get_leverage
long_lev = 1.0 if True else 1.0 # TODO-mg: Replace with self.strategy.get_leverage
short_lev = 1.0 if True else 1.0 # TODO-mg: Replace with self.strategy.get_leverage
# running get_signal on historical data fetched
(buy, sell, buy_tag) = self.strategy.get_signal(
pair,
@ -540,8 +535,7 @@ class FreqtradeBot(LoggingMixin):
:return: True if a buy order is created, false if it fails.
"""
time_in_force = self.strategy.order_time_in_force['buy'] # TODO-mg Change to enter
side = 'sell' if is_short else 'buy'
name = 'Short' if is_short else 'Buy'
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Buy']
if price:
enter_limit_requested = price
@ -576,7 +570,7 @@ class FreqtradeBot(LoggingMixin):
f"{stake_amount} ...")
amount = (stake_amount / enter_limit_requested) * leverage
order_type = self.strategy.order_types[side] # TODO-mg: Don't knoww what to do here
order_type = self.strategy.order_types["buy"] # TODO-mg: Maybe enter? or side?
if forcebuy:
# Forcebuy can define a different ordertype
# TODO-mg get a forceshort? What is this
@ -629,18 +623,9 @@ class FreqtradeBot(LoggingMixin):
amount = safe_value_fallback(order, 'filled', 'amount')
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
interest_rate = 0
interest_rate = 0.0
isolated_liq = None
if leverage > 1.0: # TODO-mg: and margin == isolated:
isolated_liq = self.exchange.get_isolated_liq(
pair=pair,
open_rate=enter_limit_filled_price,
amount=amount,
leverage=leverage,
is_short=is_short
)
if leverage > 1.0:
interest_rate = self.exchange.get_interest_rate(
pair=pair,
@ -648,6 +633,15 @@ class FreqtradeBot(LoggingMixin):
is_short=is_short
)
# TODO-mg: if margin == isolated
isolated_liq = self.exchange.get_isolated_liq(
pair=pair,
open_rate=enter_limit_filled_price,
amount=amount,
leverage=leverage,
is_short=is_short
)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
trade = Trade(
@ -963,10 +957,9 @@ class FreqtradeBot(LoggingMixin):
buy: bool, sell: bool) -> bool:
"""
Check and execute sell
# TODO-mg: Update this for shorts
"""
exit = getattr(self.strategy, "should_exit_short") if trade.is_short else getattr(
self.strategy, "should_sell")
self.strategy, "should_sell") # TODO-mg: implement should_exit_short
should_exit = exit(
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
@ -977,7 +970,7 @@ class FreqtradeBot(LoggingMixin):
# TODO-mg: Update to exit_type
logger.info(
f'Executing {trade.exit_side} for {trade.pair}. Reason: {should_exit.sell_type}')
self.execute_exit(trade, exit_rate, should_exit, side="sell")
self.execute_exit(trade, exit_rate, should_exit, side=trade.exit_side)
return True
return False
@ -1015,19 +1008,22 @@ class FreqtradeBot(LoggingMixin):
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
if (order['side'] == trade.enter_side and (
# TODO-mg: maybe change check_buy_timeout to check_enter_timeout
order['status'] == 'open' or fully_cancelled) and (
fully_cancelled
or self._check_timed_out(trade.enter_side, order)
or strategy_safe_wrapper(self.strategy.check_buy_timeout, # TODO-mg: maybe change to check_enter_timeout
or strategy_safe_wrapper(self.strategy.check_buy_timeout,
default_retval=False)(pair=trade.pair,
trade=trade,
order=order))):
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
elif (order['side'] == trade.exit_side and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled
# TODO-mg: maybe change check_sell_timeout to check_exit_timeout
elif (order['side'] == trade.exit_side and (
order['status'] == 'open' or fully_cancelled
) and (fully_cancelled
or self._check_timed_out(trade.exit_side, order)
or strategy_safe_wrapper(self.strategy.check_sell_timeout, # TODO-mg: maybe change to check_exit_timeout
or strategy_safe_wrapper(self.strategy.check_sell_timeout,
default_retval=False)(pair=trade.pair,
trade=trade,
order=order))):
@ -1058,6 +1054,7 @@ class FreqtradeBot(LoggingMixin):
Buy cancel - cancel order
:return: True if order was fully cancelled
"""
# TODO-mg: Pay back borrowed and transfer back on leveraged trades
was_trade_fully_canceled = False
# Cancelled orders may have the status of 'canceled' or 'closed'
@ -1105,9 +1102,15 @@ class FreqtradeBot(LoggingMixin):
# cancel_order may not contain the full order dict, so we need to fallback
# to the order dict aquired before cancelling.
# we need to fall back to the values from order if corder does not contain these keys.
# TODO-mg: Update trade.leverage if the full buy order was not filled
trade.amount = filled_amount
# TODO-mg: Check edge cases, we don't want to make leverage > 1.0 if we don't have to
if trade.amount <= ceil(trade.stake_amount/trade.open_rate):
# If amount is less than 1x leverage
trade.stake_amount = trade.amount * trade.open_rate
else:
# TODO: deal with situation of paying back extra amount
trade.leverage = trade.amount/trade.stake_amount
self.update_trade_state(trade, trade.open_order_id, corder)
trade.open_order_id = None
@ -1444,7 +1447,6 @@ class FreqtradeBot(LoggingMixin):
def get_real_amount(self, trade: Trade, order: Dict) -> float:
"""
TODO-mg: Update this function to account for shorts
Detect and update trade fee.
Calls trade.update_fee() upon correct detection.
Returns modified amount if the fee was taken from the destination currency.
@ -1477,7 +1479,6 @@ class FreqtradeBot(LoggingMixin):
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float:
"""
TODO-mg: Update this function to account for shorts
fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
"""
trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order),
@ -1509,6 +1510,7 @@ class FreqtradeBot(LoggingMixin):
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
# TODO-mg: leverage?
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
raise DependencyException("Half bought? Amounts don't match")

View File

@ -564,7 +564,7 @@ class RPC:
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
with self._freqtrade._sell_lock:
with self._freqtrade._exit_lock:
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.get_open_trades():
@ -626,7 +626,7 @@ class RPC:
Handler for delete <id>.
Delete the given trade and close eventually existing open orders.
"""
with self._freqtrade._sell_lock:
with self._freqtrade._exit_lock:
c_count = 0
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
if not trade:

View File

@ -1845,7 +1845,7 @@ def open_trade():
amount=90.99181073,
fee_open=0.0,
fee_close=0.0,
stake_amount=1,
stake_amount=0.001,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
)

View File

@ -2533,7 +2533,7 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker):
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
exchange.stoploss_adjust(1, {})
exchange.stoploss_adjust(1, {}, side="sell")
def test_merge_ft_has_dict(default_conf, mocker):

View File

@ -2480,6 +2480,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> N
caplog.clear()
cancel_order_mock.reset_mock()
limit_buy_order['filled'] = 2
trade.stake_amount = 0.090982711548927
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1
@ -2534,6 +2535,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order,
trade = MagicMock()
trade.pair = 'LTC/USDT'
trade.enter_side = "buy"
trade.open_rate = 200
trade.enter_side = "buy"
limit_buy_order['filled'] = 0.0
@ -2544,6 +2546,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order,
cancel_order_mock.reset_mock()
limit_buy_order['filled'] = 1.0
trade.stake_amount = 0.090982711548927
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1