updateded freqtradebot to include some margin accomadations

This commit is contained in:
Sam Germain 2021-07-24 01:19:45 -06:00
parent ec1a8a8f69
commit 13c8ee9371
3 changed files with 98 additions and 49 deletions

View File

@ -67,6 +67,7 @@ class FreqtradeBot(LoggingMixin):
init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
# TODO-mg: Do anything with this?
self.wallets = Wallets(self.config, self.exchange) self.wallets = Wallets(self.config, self.exchange)
PairLocks.timeframe = self.config['timeframe'] PairLocks.timeframe = self.config['timeframe']
@ -78,6 +79,7 @@ class FreqtradeBot(LoggingMixin):
# so anything in the Freqtradebot instance should be ready (initialized), including # so anything in the Freqtradebot instance should be ready (initialized), including
# the initial state of the bot. # the initial state of the bot.
# Keep this at the end of this initialization method. # Keep this at the end of this initialization method.
# TODO-mg: Do I need to consider the rpc, pairlists or dataprovider?
self.rpc: RPCManager = RPCManager(self) self.rpc: RPCManager = RPCManager(self)
self.pairlists = PairListManager(self.exchange, self.config) self.pairlists = PairListManager(self.exchange, self.config)
@ -100,6 +102,7 @@ class FreqtradeBot(LoggingMixin):
self.state = State[initial_state.upper()] if initial_state else State.STOPPED self.state = State[initial_state.upper()] if initial_state else State.STOPPED
# Protect sell-logic from forcesell and vice versa # Protect sell-logic from forcesell and vice versa
# TODO-mg: update to _close_lock
self._sell_lock = Lock() self._sell_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
@ -174,13 +177,15 @@ class FreqtradeBot(LoggingMixin):
self.strategy.analyze(self.active_pair_whitelist) self.strategy.analyze(self.active_pair_whitelist)
# TODO-mg: update to _close_lock
with self._sell_lock: with self._sell_lock:
# Check and handle any timed out open orders # Check and handle any timed out open orders
self.check_handle_timedout() self.check_handle_timedout()
# Protect from collisions with forcesell. # Protect from collisions with forcesell(#TODO-mg: update to forceclose).
# Without this, freqtrade my try to recreate stoploss_on_exchange orders # Without this, freqtrade my try to recreate stoploss_on_exchange orders
# while selling is in process, since telegram messages arrive in an different thread. # 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._sell_lock:
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
# First process current opened trades (positions) # First process current opened trades (positions)
@ -258,6 +263,7 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Updating {len(orders)} open orders.") logger.info(f"Updating {len(orders)} open orders.")
for order in orders: for order in orders:
try: try:
# TODO-mg: How to consider borrow orders?
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
order.ft_order_side == 'stoploss') order.ft_order_side == 'stoploss')
@ -278,49 +284,56 @@ class FreqtradeBot(LoggingMixin):
trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees() trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
for trade in trades: for trade in trades:
if not trade.is_open and not trade.fee_updated(trade.exit_side):
if not trade.is_open and not trade.fee_updated('sell'):
# Get sell fee # Get sell fee
order = trade.select_order('sell', False) order = trade.select_order(trade.exit_side, False)
if order: if order:
logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") logger.info(
f"Updating {trade.exit_side}-fee on trade {trade}"
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id, self.update_trade_state(trade, order.order_id,
stoploss_order=order.ft_order_side == 'stoploss') stoploss_order=order.ft_order_side == 'stoploss')
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
for trade in trades: for trade in trades:
if trade.is_open and not trade.fee_updated('buy'): if trade.is_open and not trade.fee_updated(trade.enter_side):
order = trade.select_order('buy', False) order = trade.select_order(trade.enter_side, False)
if order: if order:
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") logger.info(
f"Updating {trade.enter_side}-fee on trade {trade}"
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id) self.update_trade_state(trade, order.order_id)
def handle_insufficient_funds(self, trade: Trade): def handle_insufficient_funds(self, trade: Trade):
""" """
Determine if we ever opened a sell order for this trade. Determine if we ever opened a exiting order for this trade.
If not, try update buy fees - otherwise "refind" the open order we obviously lost. If not, try update entering fees - otherwise "refind" the open order we obviously lost.
""" """
sell_order = trade.select_order('sell', None) exit_order = trade.select_order(trade.exit_side, None)
if sell_order: if exit_order:
self.refind_lost_order(trade) self.refind_lost_order(trade)
else: else:
self.reupdate_buy_order_fees(trade) self.reupdate_enter_order_fees(trade)
def reupdate_buy_order_fees(self, trade: Trade): def reupdate_enter_order_fees(self, trade: Trade):
""" """
Get buy order from database, and try to reupdate. Get buy order from database, and try to reupdate.
Handles trades where the initial fee-update did not work. Handles trades where the initial fee-update did not work.
""" """
logger.info(f"Trying to reupdate buy fees for {trade}")
order = trade.select_order('buy', False) logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}")
order = trade.select_order(trade.enter_side, False)
if order: if order:
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") logger.info(
f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.")
self.update_trade_state(trade, order.order_id) self.update_trade_state(trade, order.order_id)
def refind_lost_order(self, trade): def refind_lost_order(self, trade):
""" """
Try refinding a lost trade. Try refinding a lost trade.
Only used when InsufficientFunds appears on sell orders (stoploss or sell). Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy).
Tries to walk the stored orders and sell them off eventually. Tries to walk the stored orders and sell them off eventually.
""" """
logger.info(f"Trying to refind lost order for {trade}") logger.info(f"Trying to refind lost order for {trade}")
@ -330,8 +343,8 @@ class FreqtradeBot(LoggingMixin):
if not order.ft_is_open: if not order.ft_is_open:
logger.debug(f"Order {order} is no longer open.") logger.debug(f"Order {order} is no longer open.")
continue continue
if order.ft_order_side == 'buy': if order.ft_order_side == trade.enter_side:
# Skip buy side - this is handled by reupdate_buy_order_fees # Skip buy side - this is handled by reupdate_enter_order_fees
continue continue
try: try:
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
@ -340,7 +353,7 @@ class FreqtradeBot(LoggingMixin):
if fo and fo['status'] == 'open': if fo and fo['status'] == 'open':
# Assume this as the open stoploss order # Assume this as the open stoploss order
trade.stoploss_order_id = order.order_id trade.stoploss_order_id = order.order_id
elif order.ft_order_side == 'sell': elif order.ft_order_side == trade.exit_side:
if fo and fo['status'] == 'open': if fo and fo['status'] == 'open':
# Assume this as the open order # Assume this as the open order
trade.open_order_id = order.order_id trade.open_order_id = order.order_id
@ -358,7 +371,7 @@ class FreqtradeBot(LoggingMixin):
def enter_positions(self) -> int: def enter_positions(self) -> int:
""" """
Tries to execute buy orders for new trades (positions) Tries to execute long buy/short sell orders for new trades (positions)
""" """
trades_created = 0 trades_created = 0
@ -374,7 +387,7 @@ class FreqtradeBot(LoggingMixin):
if not whitelist: if not whitelist:
logger.info("No currency pair in active pair whitelist, " logger.info("No currency pair in active pair whitelist, "
"but checking to sell open trades.") "but checking to exit open trades.")
return trades_created return trades_created
if PairLocks.is_global_lock(): if PairLocks.is_global_lock():
lock = PairLocks.get_pair_longest_lock('*') lock = PairLocks.get_pair_longest_lock('*')
@ -393,16 +406,16 @@ class FreqtradeBot(LoggingMixin):
logger.warning('Unable to create trade for %s: %s', pair, exception) logger.warning('Unable to create trade for %s: %s', pair, exception)
if not trades_created: if not trades_created:
logger.debug("Found no buy signals for whitelisted currencies. Trying again...") logger.debug("Found no enter signals for whitelisted currencies. Trying again...")
return trades_created return trades_created
def create_trade(self, pair: str) -> bool: def create_trade(self, pair: str) -> bool:
""" """
Check the implemented trading strategy for buy signals. Check the implemented trading strategy for enter signals.
If the pair triggers the buy signal a new trade record gets created If the pair triggers the enter signal a new trade record gets created
and the buy-order opening the trade gets issued towards the exchange. and the enter-order opening the trade gets issued towards the exchange.
:return: True if a trade has been created. :return: True if a trade has been created.
""" """
@ -433,17 +446,45 @@ class FreqtradeBot(LoggingMixin):
self.strategy.timeframe, self.strategy.timeframe,
analyzed_df analyzed_df
) )
(short, exit_short) = (False, False)
# = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df)
# #TODO: Update strategy class to get these values
if buy and not sell: open_buy_signal = buy and not sell
open_short_signal = short and not exit_short
# TODO: For a market making strategy, this condition should be removed
if (open_buy_signal or open_short_signal):
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
if not stake_amount:
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
return False
# leverage = 1.0 # TODO: get leverage from somewhere
logger.info(
f"{'Buy' if open_buy_signal else 'Short'} signal found: "
f"about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ..."
)
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) # TODO: All later code in this function
if ((bid_check_dom.get('enabled', False)) and if open_buy_signal:
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)): bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
if self._check_depth_of_market_buy(pair, bid_check_dom): if ((bid_check_dom.get('enabled', False)) and
return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
else: if self._check_depth_of_market_buy(pair, bid_check_dom):
return False return self.execute_buy(pair, stake_amount, buy_tag=buy_tag)
else:
return False
else:
# TODO-mg: Will check_depth_of_market_buy work for shorts?
short_check_dom = self.config.get(
'short_strategy', {}).get('check_depth_of_market', {})
if ((short_check_dom.get('enabled', False)) and
(short_check_dom.get('shorts_to_ask_delta', 0) > 0)):
if self._check_depth_of_market_short(pair, short_check_dom):
return self.execute_short(pair, stake_amount)
else:
return False
return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) return self.execute_buy(pair, stake_amount, buy_tag=buy_tag)
else: else:
@ -473,6 +514,10 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
return False return False
def _check_depth_of_market_short(self, pair: str, conf: Dict) -> bool:
# TODO-mg: implement, use _check_depth_of_market_buy instead if possible
return False
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None, def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None,
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool:
""" """
@ -596,6 +641,10 @@ class FreqtradeBot(LoggingMixin):
return True return True
def execute_short(self, pair: str, stake_amount: float, price: Optional[float] = None,
forcesell: bool = False) -> bool:
return False # TODO-mg: implement
def _notify_buy(self, trade: Trade, order_type: str) -> None: def _notify_buy(self, trade: Trade, order_type: str) -> None:
""" """
Sends rpc notification when a buy occurred. Sends rpc notification when a buy occurred.

View File

@ -28,13 +28,13 @@ class PairListManager():
self._tickers_needed = False self._tickers_needed = False
for pairlist_handler_config in self._config.get('pairlists', None): for pairlist_handler_config in self._config.get('pairlists', None):
pairlist_handler = PairListResolver.load_pairlist( pairlist_handler = PairListResolver.load_pairlist(
pairlist_handler_config['method'], pairlist_handler_config['method'],
exchange=exchange, exchange=exchange,
pairlistmanager=self, pairlistmanager=self,
config=config, config=config,
pairlistconfig=pairlist_handler_config, pairlistconfig=pairlist_handler_config,
pairlist_pos=len(self._pairlist_handlers) pairlist_pos=len(self._pairlist_handlers)
) )
self._tickers_needed |= pairlist_handler.needstickers self._tickers_needed |= pairlist_handler.needstickers
self._pairlist_handlers.append(pairlist_handler) self._pairlist_handlers.append(pairlist_handler)
@ -127,7 +127,7 @@ class PairListManager():
:return: pairlist - whitelisted pairs :return: pairlist - whitelisted pairs
""" """
try: try:
# TODO-mg: filter for pairlists that are able to trade at the desired leverage
whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid) whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid)
except ValueError as err: except ValueError as err:
logger.error(f"Pair whitelist contains an invalid Wildcard: {err}") logger.error(f"Pair whitelist contains an invalid Wildcard: {err}")

View File

@ -1625,7 +1625,7 @@ def test_enter_positions(mocker, default_conf, caplog) -> None:
MagicMock(return_value=False)) MagicMock(return_value=False))
n = freqtrade.enter_positions() n = freqtrade.enter_positions()
assert n == 0 assert n == 0
assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) assert log_has('Found no enter signals for whitelisted currencies. Trying again...', caplog)
# create_trade should be called once for every pair in the whitelist. # create_trade should be called once for every pair in the whitelist.
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
@ -4332,14 +4332,14 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog):
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state')
create_mock_trades(fee) create_mock_trades(fee)
trades = Trade.get_trades().all() trades = Trade.get_trades().all()
freqtrade.reupdate_buy_order_fees(trades[0]) freqtrade.reupdate_enter_order_fees(trades[0])
assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
assert mock_uts.call_count == 1 assert mock_uts.call_count == 1
assert mock_uts.call_args_list[0][0][0] == trades[0] assert mock_uts.call_args_list[0][0][0] == trades[0]
@ -4362,7 +4362,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog):
) )
Trade.query.session.add(trade) Trade.query.session.add(trade)
freqtrade.reupdate_buy_order_fees(trade) freqtrade.reupdate_enter_order_fees(trade)
assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
assert mock_uts.call_count == 0 assert mock_uts.call_count == 0
assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog)
@ -4372,7 +4372,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog):
def test_handle_insufficient_funds(mocker, default_conf, fee): def test_handle_insufficient_funds(mocker, default_conf, fee):
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order')
mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees') mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees')
create_mock_trades(fee) create_mock_trades(fee)
trades = Trade.get_trades().all() trades = Trade.get_trades().all()