예제 #1
0
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
    """
    Test send_msg() method with Telegram disabled
    """
    telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
    mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())

    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    rpc_manager = RPCManager(freqtradebot)
    rpc_manager.send_msg('test')

    assert log_has('Sending rpc message: test', caplog.record_tuples)
    assert telegram_mock.call_count == 1
예제 #2
0
def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
    """
    Test send_msg() method with Telegram disabled
    """
    telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg',
                                 MagicMock())

    conf = deepcopy(default_conf)
    conf['telegram']['enabled'] = False

    freqtradebot = get_patched_freqtradebot(mocker, conf)
    rpc_manager = RPCManager(freqtradebot)
    rpc_manager.send_msg('test')

    assert log_has('test', caplog.record_tuples)
    assert telegram_mock.call_count == 0
예제 #3
0
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
    """
    Test cleanup() method with Telegram disabled
    """
    caplog.set_level(logging.DEBUG)
    telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())

    conf = deepcopy(default_conf)
    conf['telegram']['enabled'] = False

    freqtradebot = get_patched_freqtradebot(mocker, conf)
    rpc_manager = RPCManager(freqtradebot)
    rpc_manager.cleanup()

    assert not log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
    assert telegram_mock.call_count == 0
예제 #4
0
def test__init__(mocker, default_conf) -> None:
    """ Test __init__() method """
    conf = deepcopy(default_conf)
    conf['telegram']['enabled'] = False

    rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
    assert rpc_manager.registered_modules == []
예제 #5
0
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
    """
    Test cleanup() method with Telegram enabled
    """
    caplog.set_level(logging.DEBUG)
    mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
    telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())

    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    rpc_manager = RPCManager(freqtradebot)

    # Check we have Telegram as a registered modules
    assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]

    rpc_manager.cleanup()
    assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
    assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
    assert telegram_mock.call_count == 1
예제 #6
0
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
    """ Test _init() method with Telegram disabled """
    caplog.set_level(logging.DEBUG)

    conf = deepcopy(default_conf)
    conf['telegram']['enabled'] = False

    rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))

    assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
    assert rpc_manager.registered_modules == []
예제 #7
0
    def _init_modules(self, db_url: Optional[str] = None) -> None:
        """
        Initializes all modules and updates the config
        :param db_url: database connector string for sqlalchemy (Optional)
        :return: None
        """
        # Initialize all modules
        self.analyze = Analyze(self.config)
        self.fiat_converter = CryptoToFiatConverter()
        self.rpc = RPCManager(self)

        persistence.init(self.config, db_url)
        exchange.init(self.config)

        # Set initial application state
        initial_state = self.config.get('initial_state')

        if initial_state:
            self.state = State[initial_state.upper()]
        else:
            self.state = State.STOPPED
예제 #8
0
def test__init__(mocker, default_conf) -> None:
    """
    Test __init__() method
    """
    init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init',
                             MagicMock())
    freqtradebot = get_patched_freqtradebot(mocker, default_conf)

    rpc_manager = RPCManager(freqtradebot)
    assert rpc_manager.freqtrade == freqtradebot
    assert rpc_manager.registered_modules == []
    assert rpc_manager.telegram is None
    assert init_mock.call_count == 1
예제 #9
0
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
    """
    Test _init() method with Telegram enabled
    """
    caplog.set_level(logging.DEBUG)
    mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())

    rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))

    assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
    len_modules = len(rpc_manager.registered_modules)
    assert len_modules == 1
    assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
예제 #10
0
    def __init__(self, config: Dict[str, Any]) -> None:
        """
        Init all variables and object the bot need to work
        :param config: configuration dict, you can use the Configuration.get_config()
        method to get the config dict.
        """

        logger.info(
            'Starting freqtrade %s',
            __version__,
        )

        # Init bot states
        self.state = State.STOPPED

        # Init objects
        self.config = config
        self.analyze = Analyze(self.config)
        self.fiat_converter = CryptoToFiatConverter()
        self.rpc: RPCManager = RPCManager(self)
        self.persistence = None
        self.exchange = Exchange(self.config)

        self._init_modules()
예제 #11
0
class FreqtradeBot(object):
    """
    Freqtrade is the main class of the bot.
    This is from here the bot start its logic.
    """
    def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None):
        """
        Init all variables and object the bot need to work
        :param config: configuration dict, you can use the Configuration.get_config()
        method to get the config dict.
        :param db_url: database connector string for sqlalchemy (Optional)
        """

        logger.info(
            'Starting freqtrade %s',
            __version__,
        )

        # Init bot states
        self.state = State.STOPPED

        # Init objects
        self.config = config
        self.analyze = None
        self.fiat_converter = None
        self.rpc = None
        self.persistence = None
        self.exchange = None

        self._init_modules(db_url=db_url)

    def _init_modules(self, db_url: Optional[str] = None) -> None:
        """
        Initializes all modules and updates the config
        :param db_url: database connector string for sqlalchemy (Optional)
        :return: None
        """
        # Initialize all modules
        self.analyze = Analyze(self.config)
        self.fiat_converter = CryptoToFiatConverter()
        self.rpc = RPCManager(self)

        persistence.init(self.config, db_url)
        exchange.init(self.config)

        # Set initial application state
        initial_state = self.config.get('initial_state')

        if initial_state:
            self.state = State[initial_state.upper()]
        else:
            self.state = State.STOPPED

    def clean(self) -> bool:
        """
        Cleanup the application state und finish all pending tasks
        :return: None
        """
        self.rpc.send_msg('*Status:* `Stopping trader...`')
        logger.info('Stopping trader and cleaning up modules...')
        self.state = State.STOPPED
        self.rpc.cleanup()
        persistence.cleanup()
        return True

    def worker(self, old_state: None) -> State:
        """
        Trading routine that must be run at each loop
        :param old_state: the previous service state from the previous call
        :return: current service state
        """
        # Log state transition
        state = self.state
        if state != old_state:
            self.rpc.send_msg('*Status:* `{}`'.format(state.name.lower()))
            logger.info('Changing state to: %s', state.name)

        if state == State.STOPPED:
            time.sleep(1)
        elif state == State.RUNNING:
            min_secs = self.config.get('internals',
                                       {}).get('process_throttle_secs',
                                               constants.PROCESS_THROTTLE_SECS)

            nb_assets = self.config.get('dynamic_whitelist', None)

            self._throttle(func=self._process,
                           min_secs=min_secs,
                           nb_assets=nb_assets)
        return state

    def _throttle(self, func: Callable[..., Any], min_secs: float, *args,
                  **kwargs) -> Any:
        """
        Throttles the given callable that it
        takes at least `min_secs` to finish execution.
        :param func: Any callable
        :param min_secs: minimum execution time in seconds
        :return: Any
        """
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        duration = max(min_secs - (end - start), 0.0)
        logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
        time.sleep(duration)
        return result

    def _process(self, nb_assets: Optional[int] = 0) -> bool:
        """
        Queries the persistence layer for open trades and handles them,
        otherwise a new trade is created.
        :param: nb_assets: the maximum number of pairs to be traded at the same time
        :return: True if one or more trades has been created or closed, False otherwise
        """
        state_changed = False
        try:
            # Refresh whitelist based on wallet maintenance
            sanitized_list = self._refresh_whitelist(
                self._gen_pair_whitelist(self.config['stake_currency'])
                if nb_assets else self.config['exchange']['pair_whitelist'])

            # Keep only the subsets of pairs wanted (up to nb_assets)
            final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
            self.config['exchange']['pair_whitelist'] = final_list

            # Query trades from persistence layer
            trades = Trade.query.filter(Trade.is_open.is_(True)).all()

            # First process current opened trades
            for trade in trades:
                state_changed |= self.process_maybe_execute_sell(trade)

            # Then looking for buy opportunities
            if len(trades) < self.config['max_open_trades']:
                state_changed = self.process_maybe_execute_buy()

            if 'unfilledtimeout' in self.config:
                # Check and handle any timed out open orders
                self.check_handle_timedout(self.config['unfilledtimeout'])
                Trade.session.flush()

        except (requests.exceptions.RequestException,
                json.JSONDecodeError) as error:
            logger.warning('%s, retrying in 30 seconds...', error)
            time.sleep(constants.RETRY_TIMEOUT)
        except OperationalException:
            self.rpc.send_msg(
                '*Status:* OperationalException:\n```\n{traceback}```{hint}'.
                format(
                    traceback=traceback.format_exc(),
                    hint='Issue `/start` if you think it is safe to restart.'))
            logger.exception('OperationalException. Stopping trader ...')
            self.state = State.STOPPED
        return state_changed

    @cached(TTLCache(maxsize=1, ttl=1800))
    def _gen_pair_whitelist(self,
                            base_currency: str,
                            key: str = 'BaseVolume') -> List[str]:
        """
        Updates the whitelist with with a dynamically generated list
        :param base_currency: base currency as str
        :param key: sort key (defaults to 'BaseVolume')
        :return: List of pairs
        """
        summaries = sorted((s for s in exchange.get_market_summaries()
                            if s['MarketName'].startswith(base_currency)),
                           key=lambda s: s.get(key) or 0.0,
                           reverse=True)

        return [s['MarketName'].replace('-', '_') for s in summaries]

    def _refresh_whitelist(self, whitelist: List[str]) -> List[str]:
        """
        Check wallet health and remove pair from whitelist if necessary
        :param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to
        trade
        :return: the list of pairs the user wants to trade without the one unavailable or
        black_listed
        """
        sanitized_whitelist = whitelist
        health = exchange.get_wallet_health()
        known_pairs = set()
        for status in health:
            pair = '{}_{}'.format(self.config['stake_currency'],
                                  status['Currency'])
            # pair is not int the generated dynamic market, or in the blacklist ... ignore it
            if pair not in whitelist or pair in self.config['exchange'].get(
                    'pair_blacklist', []):
                continue
            # else the pair is valid
            known_pairs.add(pair)
            # Market is not active
            if not status['IsActive']:
                sanitized_whitelist.remove(pair)
                logger.info('Ignoring %s from whitelist (reason: %s).', pair,
                            status.get('Notice') or 'wallet is not active')

        # We need to remove pairs that are unknown
        final_list = [x for x in sanitized_whitelist if x in known_pairs]
        return final_list

    def get_target_bid(self, ticker: Dict[str, float]) -> float:
        """
        Calculates bid target between current ask price and last price
        :param ticker: Ticker to use for getting Ask and Last Price
        :return: float: Price
        """
        if ticker['ask'] < ticker['last']:
            return ticker['ask']
        balance = self.config['bid_strategy']['ask_last_balance']
        return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])

    def create_trade(self) -> bool:
        """
        Checks the implemented trading indicator(s) for a randomly picked pair,
        if one pair triggers the buy_signal a new trade record gets created
        :param stake_amount: amount of btc to spend
        :param interval: Ticker interval used for Analyze
        :return: True if a trade object has been created and persisted, False otherwise
        """
        stake_amount = self.config['stake_amount']
        interval = self.analyze.get_ticker_interval()

        logger.info(
            'Checking buy signals to create a new trade with stake_amount: %f ...',
            stake_amount)
        whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
        # Check if stake_amount is fulfilled
        if exchange.get_balance(self.config['stake_currency']) < stake_amount:
            raise DependencyException(
                'stake amount is not fulfilled (currency={})'.format(
                    self.config['stake_currency']))

        # Remove currently opened and latest pairs from whitelist
        for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
            if trade.pair in whitelist:
                whitelist.remove(trade.pair)
                logger.debug('Ignoring %s in pair whitelist', trade.pair)

        if not whitelist:
            raise DependencyException('No currency pairs in whitelist')

        # Pick pair based on StochRSI buy signals
        for _pair in whitelist:
            (buy, sell) = self.analyze.get_signal(_pair, interval)
            if buy and not sell:
                pair = _pair
                break
        else:
            return False

        # Calculate amount
        buy_limit = self.get_target_bid(exchange.get_ticker(pair))
        amount = stake_amount / buy_limit

        order_id = exchange.buy(pair, buy_limit, amount)

        stake_amount_fiat = self.fiat_converter.convert_amount(
            stake_amount, self.config['stake_currency'],
            self.config['fiat_display_currency'])

        # Create trade entity and return
        self.rpc.send_msg(
            '*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '
            .format(exchange.get_name().upper(), pair.replace('_', '/'),
                    exchange.get_pair_detail_url(pair), buy_limit,
                    stake_amount, self.config['stake_currency'],
                    stake_amount_fiat, self.config['fiat_display_currency']))
        # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
        trade = Trade(pair=pair,
                      stake_amount=stake_amount,
                      amount=amount,
                      fee=exchange.get_fee(),
                      open_rate=buy_limit,
                      open_date=datetime.utcnow(),
                      exchange=exchange.get_name().upper(),
                      open_order_id=order_id)
        Trade.session.add(trade)
        Trade.session.flush()
        return True

    def process_maybe_execute_buy(self) -> bool:
        """
        Tries to execute a buy trade in a safe way
        :return: True if executed
        """
        try:
            # Create entity and execute trade
            if self.create_trade():
                return True

            logger.info(
                'Found no buy signals for whitelisted currencies. Trying again..'
            )
            return False
        except DependencyException as exception:
            logger.warning('Unable to create trade: %s', exception)
            return False

    def process_maybe_execute_sell(self, trade: Trade) -> bool:
        """
        Tries to execute a sell trade
        :return: True if executed
        """
        # Get order details for actual price per unit
        if trade.open_order_id:
            # Update trade with order values
            logger.info('Found open order for %s', trade)
            trade.update(exchange.get_order(trade.open_order_id))

        if trade.is_open and trade.open_order_id is None:
            # Check if we can sell our current pair
            return self.handle_trade(trade)
        return False

    def handle_trade(self, trade: Trade) -> bool:
        """
        Sells the current pair if the threshold is reached and updates the trade record.
        :return: True if trade has been sold, False otherwise
        """
        if not trade.is_open:
            raise ValueError(
                'attempt to handle closed trade: {}'.format(trade))

        logger.debug('Handling %s ...', trade)
        current_rate = exchange.get_ticker(trade.pair)['bid']

        (buy, sell) = (False, False)

        if self.config.get('experimental', {}).get('use_sell_signal'):
            (buy, sell) = self.analyze.get_signal(
                trade.pair, self.analyze.get_ticker_interval())

        if self.analyze.should_sell(trade, current_rate, datetime.utcnow(),
                                    buy, sell):
            self.execute_sell(trade, current_rate)
            return True

        return False

    def check_handle_timedout(self, timeoutvalue: int) -> None:
        """
        Check if any orders are timed out and cancel if neccessary
        :param timeoutvalue: Number of minutes until order is considered timed out
        :return: None
        """
        timeoutthreashold = arrow.utcnow().shift(
            minutes=-timeoutvalue).datetime

        for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
            try:
                order = exchange.get_order(trade.open_order_id)
            except requests.exceptions.RequestException:
                logger.info('Cannot query order for %s due to %s', trade,
                            traceback.format_exc())
                continue
            ordertime = arrow.get(order['opened'])

            # Check if trade is still actually open
            if int(order['remaining']) == 0:
                continue

            if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
                self.handle_timedout_limit_buy(trade, order)
            elif order[
                    'type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
                self.handle_timedout_limit_sell(trade, order)

    # FIX: 20180110, why is cancel.order unconditionally here, whereas
    #                it is conditionally called in the
    #                handle_timedout_limit_sell()?
    def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
        """Buy timeout - cancel order
        :return: True if order was fully cancelled
        """
        exchange.cancel_order(trade.open_order_id)
        if order['remaining'] == order['amount']:
            # if trade is not partially completed, just delete the trade
            Trade.session.delete(trade)
            # FIX? do we really need to flush, caller of
            #      check_handle_timedout will flush afterwards
            Trade.session.flush()
            logger.info('Buy order timeout for %s.', trade)
            self.rpc.send_msg(
                '*Timeout:* Unfilled buy order for {} cancelled'.format(
                    trade.pair.replace('_', '/')))
            return True

        # if trade is partially complete, edit the stake details for the trade
        # and close the order
        trade.amount = order['amount'] - order['remaining']
        trade.stake_amount = trade.amount * trade.open_rate
        trade.open_order_id = None
        logger.info('Partial buy order timeout for %s.', trade)
        self.rpc.send_msg(
            '*Timeout:* Remaining buy order for {} cancelled'.format(
                trade.pair.replace('_', '/')))
        return False

    # FIX: 20180110, should cancel_order() be cond. or unconditionally called?
    def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
        """
        Sell timeout - cancel order and update trade
        :return: True if order was fully cancelled
        """
        if order['remaining'] == order['amount']:
            # if trade is not partially completed, just cancel the trade
            exchange.cancel_order(trade.open_order_id)
            trade.close_rate = None
            trade.close_profit = None
            trade.close_date = None
            trade.is_open = True
            trade.open_order_id = None
            self.rpc.send_msg(
                '*Timeout:* Unfilled sell order for {} cancelled'.format(
                    trade.pair.replace('_', '/')))
            logger.info('Sell order timeout for %s.', trade)
            return True

        # TODO: figure out how to handle partially complete sell orders
        return False

    def execute_sell(self, trade: Trade, limit: float) -> None:
        """
        Executes a limit sell for the given trade and limit
        :param trade: Trade instance
        :param limit: limit rate for the sell order
        :return: None
        """
        # Execute sell and update trade record
        order_id = exchange.sell(str(trade.pair), limit, trade.amount)
        trade.open_order_id = order_id

        fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
        profit_trade = trade.calc_profit(rate=limit)
        current_rate = exchange.get_ticker(trade.pair, False)['bid']
        profit = trade.calc_profit_percent(current_rate)

        message = "*{exchange}:* Selling\n" \
                  "*Current Pair:* [{pair}]({pair_url})\n" \
                  "*Limit:* `{limit}`\n" \
                  "*Amount:* `{amount}`\n" \
                  "*Open Rate:* `{open_rate:.8f}`\n" \
                  "*Current Rate:* `{current_rate:.8f}`\n" \
                  "*Profit:* `{profit:.2f}%`" \
                  "".format(
                      exchange=trade.exchange,
                      pair=trade.pair,
                      pair_url=exchange.get_pair_detail_url(trade.pair),
                      limit=limit,
                      open_rate=trade.open_rate,
                      current_rate=current_rate,
                      amount=round(trade.amount, 8),
                      profit=round(profit * 100, 2),
                  )

        # For regular case, when the configuration exists
        if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
            fiat_converter = CryptoToFiatConverter()
            profit_fiat = fiat_converter.convert_amount(
                profit_trade, self.config['stake_currency'],
                self.config['fiat_display_currency'])
            message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
                       '` / {profit_fiat:.3f} {fiat})`' \
                       ''.format(
                           gain="profit" if fmt_exp_profit > 0 else "loss",
                           profit_percent=fmt_exp_profit,
                           profit_coin=profit_trade,
                           coin=self.config['stake_currency'],
                           profit_fiat=profit_fiat,
                           fiat=self.config['fiat_display_currency'],
                       )
        # Because telegram._forcesell does not have the configuration
        # Ignore the FIAT value and does not show the stake_currency as well
        else:
            message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
                gain="profit" if fmt_exp_profit > 0 else "loss",
                profit_percent=fmt_exp_profit,
                profit_coin=profit_trade)

        # Send the message
        self.rpc.send_msg(message)
        Trade.session.flush()