def test_convert_amount(mocker):
    patch_coinmarketcap(mocker)
    mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price',
                 return_value=12345.0)

    fiat_convert = CryptoToFiatConverter()
    result = fiat_convert.convert_amount(crypto_amount=1.23,
                                         crypto_symbol="BTC",
                                         fiat_symbol="USD")
    assert result == 15184.35

    result = fiat_convert.convert_amount(crypto_amount=1.23,
                                         crypto_symbol="BTC",
                                         fiat_symbol="BTC")
    assert result == 1.23
Beispiel #2
0
    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
        """
        exc = trade.exchange
        pair = trade.pair
        # Execute sell and update trade record
        order_id = self.exchange.sell(str(trade.pair), limit,
                                      trade.amount)['id']
        trade.open_order_id = order_id
        trade.close_rate_requested = limit

        fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
        profit_trade = trade.calc_profit(rate=limit)
        current_rate = self.exchange.get_ticker(trade.pair)['bid']
        profit = trade.calc_profit_percent(limit)
        pair_url = self.exchange.get_pair_detail_url(trade.pair)
        gain = "profit" if fmt_exp_profit > 0 else "loss"

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

        # For regular case, when the configuration exists
        if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
            stake = self.config['stake_currency']
            fiat = self.config['fiat_display_currency']
            fiat_converter = CryptoToFiatConverter()
            profit_fiat = fiat_converter.convert_amount(
                profit_trade, stake, fiat)
            message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
                       f'` / {profit_fiat:.3f} {fiat})`'\
                       ''
        # 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()
Beispiel #3
0
def execute_sell(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)

    message = '*{exchange}:* Selling [{pair}]({pair_url}) with limit `{limit:.8f}`'.format(
        exchange=trade.exchange,
        pair=trade.pair.replace('_', '/'),
        pair_url=exchange.get_pair_detail_url(trade.pair),
        limit=limit
    )

    # For regular case, when the configuration exists
    if 'stake_currency' in _CONF and 'fiat_display_currency' in _CONF:
        fiat_converter = CryptoToFiatConverter()
        profit_fiat = fiat_converter.convert_amount(
            profit_trade,
            _CONF['stake_currency'],
            _CONF['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=_CONF['stake_currency'],
                       profit_fiat=profit_fiat,
                       fiat=_CONF['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
    rpc.send_msg(message)
    Trade.session.flush()
Beispiel #4
0
def execute_sell(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)

    message = '*{exchange}:* Selling [{pair}]({pair_url}) with limit `{limit:.8f}`'.format(
        exchange=trade.exchange,
        pair=trade.pair.replace('_', '/'),
        pair_url=exchange.get_pair_detail_url(trade.pair),
        limit=limit)

    # For regular case, when the configuration exists
    if 'stake_currency' in _CONF and 'fiat_display_currency' in _CONF:
        fiat_converter = CryptoToFiatConverter()
        profit_fiat = fiat_converter.convert_amount(
            profit_trade, _CONF['stake_currency'],
            _CONF['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=_CONF['stake_currency'],
                       profit_fiat=profit_fiat,
                       fiat=_CONF['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
    rpc.send_msg(message)
    Trade.session.flush()
Beispiel #5
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()
Beispiel #6
0
class Telegram(RPC):
    """  This class handles all telegram communication """
    def __init__(self, freqtrade) -> None:
        """
        Init the Telegram call, and init the super class RPC
        :param freqtrade: Instance of a freqtrade bot
        :return: None
        """
        super().__init__(freqtrade)

        self._updater: Updater = None
        self._config = freqtrade.config
        self._init()
        if self._config.get('fiat_display_currency', None):
            self._fiat_converter = CryptoToFiatConverter()

    def _init(self) -> None:
        """
        Initializes this module with the given config,
        registers all known command handlers
        and starts polling for message updates
        """
        self._updater = Updater(token=self._config['telegram']['token'],
                                workers=0)

        # Register command handler and start telegram message polling
        handles = [
            CommandHandler('status', self._status),
            CommandHandler('profit', self._profit),
            CommandHandler('balance', self._balance),
            CommandHandler('start', self._start),
            CommandHandler('stop', self._stop),
            CommandHandler('forcesell', self._forcesell),
            CommandHandler('performance', self._performance),
            CommandHandler('daily', self._daily),
            CommandHandler('count', self._count),
            CommandHandler('reload_conf', self._reload_conf),
            CommandHandler('help', self._help),
            CommandHandler('version', self._version),
        ]
        for handle in handles:
            self._updater.dispatcher.add_handler(handle)
        self._updater.start_polling(
            clean=True,
            bootstrap_retries=-1,
            timeout=30,
            read_latency=60,
        )
        logger.info('rpc.telegram is listening for following commands: %s',
                    [h.command for h in handles])

    def cleanup(self) -> None:
        """
        Stops all running telegram threads.
        :return: None
        """
        self._updater.stop()

    def send_msg(self, msg: Dict[str, Any]) -> None:
        """ Send a message to telegram channel """

        if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
            if self._fiat_converter:
                msg['stake_amount_fiat'] = self._fiat_converter.convert_amount(
                    msg['stake_amount'], msg['stake_currency'],
                    msg['fiat_currency'])
            else:
                msg['stake_amount_fiat'] = 0

            message = "*{exchange}:* Buying [{pair}]({market_url})\n" \
                      "with limit `{limit:.8f}\n" \
                      "({stake_amount:.6f} {stake_currency}".format(**msg)

            if msg.get('fiat_currency', None):
                message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(
                    **msg)
            message += ")`"

        elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
            msg['amount'] = round(msg['amount'], 8)
            msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)

            message = "*{exchange}:* Selling [{pair}]({market_url})\n" \
                      "*Limit:* `{limit:.8f}`\n" \
                      "*Amount:* `{amount:.8f}`\n" \
                      "*Open Rate:* `{open_rate:.8f}`\n" \
                      "*Current Rate:* `{current_rate:.8f}`\n" \
                      "*Profit:* `{profit_percent:.2f}%`".format(**msg)

            # Check if all sell properties are available.
            # This might not be the case if the message origin is triggered by /forcesell
            if (all(prop in msg
                    for prop in ['gain', 'fiat_currency', 'stake_currency'])
                    and self._fiat_converter):
                msg['profit_fiat'] = self._fiat_converter.convert_amount(
                    msg['profit_amount'], msg['stake_currency'],
                    msg['fiat_currency'])
                message += '` ({gain}: {profit_amount:.8f} {stake_currency}`' \
                           '` / {profit_fiat:.3f} {fiat_currency})`'.format(**msg)

        elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
            message = '*Status:* `{status}`'.format(**msg)

        elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
            message = '*Warning:* `{status}`'.format(**msg)

        elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
            message = '{status}'.format(**msg)

        else:
            raise NotImplementedError('Unknown message type: {}'.format(
                msg['type']))

        self._send_msg(message)

    @authorized_only
    def _status(self, bot: Bot, update: Update) -> None:
        """
        Handler for /status.
        Returns the current TradeThread status
        :param bot: telegram bot
        :param update: message update
        :return: None
        """

        # Check if additional parameters are passed
        params = update.message.text.replace('/status', '').split(' ') \
            if update.message.text else []
        if 'table' in params:
            self._status_table(bot, update)
            return

        try:
            results = self._rpc_trade_status()
            # pre format data
            for result in results:
                result['date'] = result['date'].humanize()

            messages = [
                "*Trade ID:* `{trade_id}`\n"
                "*Current Pair:* [{pair}]({market_url})\n"
                "*Open Since:* `{date}`\n"
                "*Amount:* `{amount}`\n"
                "*Open Rate:* `{open_rate:.8f}`\n"
                "*Close Rate:* `{close_rate}`\n"
                "*Current Rate:* `{current_rate:.8f}`\n"
                "*Close Profit:* `{close_profit}`\n"
                "*Current Profit:* `{current_profit:.2f}%`\n"
                "*Open Order:* `{open_order}`".format(**result)
                for result in results
            ]
            for msg in messages:
                self._send_msg(msg, bot=bot)
        except RPCException as e:
            self._send_msg(str(e), bot=bot)

    @authorized_only
    def _status_table(self, bot: Bot, update: Update) -> None:
        """
        Handler for /status table.
        Returns the current TradeThread status in table format
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        try:
            df_statuses = self._rpc_status_table()
            message = tabulate(df_statuses, headers='keys', tablefmt='simple')
            self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
        except RPCException as e:
            self._send_msg(str(e), bot=bot)

    @authorized_only
    def _daily(self, bot: Bot, update: Update) -> None:
        """
        Handler for /daily <n>
        Returns a daily profit (in BTC) over the last n days.
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        stake_cur = self._config['stake_currency']
        fiat_disp_cur = self._config.get('fiat_display_currency', '')
        try:
            timescale = int(update.message.text.replace('/daily', '').strip())
        except (TypeError, ValueError):
            timescale = 7
        try:
            stats = self._rpc_daily_profit(timescale, stake_cur, fiat_disp_cur)
            stats = tabulate(stats,
                             headers=[
                                 'Day', f'Profit {stake_cur}',
                                 f'Profit {fiat_disp_cur}'
                             ],
                             tablefmt='simple')
            message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats}</pre>'
            self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
        except RPCException as e:
            self._send_msg(str(e), bot=bot)

    @authorized_only
    def _profit(self, bot: Bot, update: Update) -> None:
        """
        Handler for /profit.
        Returns a cumulative profit statistics.
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        stake_cur = self._config['stake_currency']
        fiat_disp_cur = self._config.get('fiat_display_currency', '')

        try:
            stats = self._rpc_trade_statistics(stake_cur, fiat_disp_cur)
            profit_closed_coin = stats['profit_closed_coin']
            profit_closed_percent = stats['profit_closed_percent']
            profit_closed_fiat = stats['profit_closed_fiat']
            profit_all_coin = stats['profit_all_coin']
            profit_all_percent = stats['profit_all_percent']
            profit_all_fiat = stats['profit_all_fiat']
            trade_count = stats['trade_count']
            first_trade_date = stats['first_trade_date']
            latest_trade_date = stats['latest_trade_date']
            avg_duration = stats['avg_duration']
            best_pair = stats['best_pair']
            best_rate = stats['best_rate']
            # Message to display
            markdown_msg = "*ROI:* Close trades\n" \
                           f"∙ `{profit_closed_coin:.8f} {stake_cur} "\
                           f"({profit_closed_percent:.2f}%)`\n" \
                           f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n" \
                           f"*ROI:* All trades\n" \
                           f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" \
                           f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" \
                           f"*Total Trade Count:* `{trade_count}`\n" \
                           f"*First Trade opened:* `{first_trade_date}`\n" \
                           f"*Latest Trade opened:* `{latest_trade_date}`\n" \
                           f"*Avg. Duration:* `{avg_duration}`\n" \
                           f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
            self._send_msg(markdown_msg, bot=bot)
        except RPCException as e:
            self._send_msg(str(e), bot=bot)

    @authorized_only
    def _balance(self, bot: Bot, update: Update) -> None:
        """ Handler for /balance """
        try:
            result = self._rpc_balance(
                self._config.get('fiat_display_currency', ''))
            output = ''
            for currency in result['currencies']:
                output += "*{currency}:*\n" \
                          "\t`Available: {available: .8f}`\n" \
                          "\t`Balance: {balance: .8f}`\n" \
                          "\t`Pending: {pending: .8f}`\n" \
                          "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)

            output += "\n*Estimated Value*:\n" \
                      "\t`BTC: {total: .8f}`\n" \
                      "\t`{symbol}: {value: .2f}`\n".format(**result)
            self._send_msg(output, bot=bot)
        except RPCException as e:
            self._send_msg(str(e), bot=bot)

    @authorized_only
    def _start(self, bot: Bot, update: Update) -> None:
        """
        Handler for /start.
        Starts TradeThread
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        msg = self._rpc_start()
        self._send_msg('Status: `{status}`'.format(**msg), bot=bot)

    @authorized_only
    def _stop(self, bot: Bot, update: Update) -> None:
        """
        Handler for /stop.
        Stops TradeThread
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        msg = self._rpc_stop()
        self._send_msg('Status: `{status}`'.format(**msg), bot=bot)

    @authorized_only
    def _reload_conf(self, bot: Bot, update: Update) -> None:
        """
        Handler for /reload_conf.
        Triggers a config file reload
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        msg = self._rpc_reload_conf()
        self._send_msg('Status: `{status}`'.format(**msg), bot=bot)

    @authorized_only
    def _forcesell(self, bot: Bot, update: Update) -> None:
        """
        Handler for /forcesell <id>.
        Sells the given trade at current price
        :param bot: telegram bot
        :param update: message update
        :return: None
        """

        trade_id = update.message.text.replace('/forcesell', '').strip()
        try:
            self._rpc_forcesell(trade_id)
        except RPCException as e:
            self._send_msg(str(e), bot=bot)

    @authorized_only
    def _performance(self, bot: Bot, update: Update) -> None:
        """
        Handler for /performance.
        Shows a performance statistic from finished trades
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        try:
            trades = self._rpc_performance()
            stats = '\n'.join(
                '{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.
                format(index=i + 1,
                       pair=trade['pair'],
                       profit=trade['profit'],
                       count=trade['count']) for i, trade in enumerate(trades))
            message = '<b>Performance:</b>\n{}'.format(stats)
            self._send_msg(message, parse_mode=ParseMode.HTML)
        except RPCException as e:
            self._send_msg(str(e), bot=bot)

    @authorized_only
    def _count(self, bot: Bot, update: Update) -> None:
        """
        Handler for /count.
        Returns the number of trades running
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        try:
            trades = self._rpc_count()
            message = tabulate(
                {
                    'current': [len(trades)],
                    'max': [self._config['max_open_trades']],
                    'total stake': [
                        sum((trade.open_rate * trade.amount)
                            for trade in trades)
                    ]
                },
                headers=['current', 'max', 'total stake'],
                tablefmt='simple')
            message = "<pre>{}</pre>".format(message)
            logger.debug(message)
            self._send_msg(message, parse_mode=ParseMode.HTML)
        except RPCException as e:
            self._send_msg(str(e), bot=bot)

    @authorized_only
    def _help(self, bot: Bot, update: Update) -> None:
        """
        Handler for /help.
        Show commands of the bot
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        message = "*/start:* `Starts the trader`\n" \
                  "*/stop:* `Stops the trader`\n" \
                  "*/status [table]:* `Lists all open trades`\n" \
                  "         *table :* `will display trades in a table`\n" \
                  "*/profit:* `Lists cumulative profit from all finished trades`\n" \
                  "*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " \
                  "regardless of profit`\n" \
                  "*/performance:* `Show performance of each finished trade grouped by pair`\n" \
                  "*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" \
                  "*/count:* `Show number of trades running compared to allowed number of trades`" \
                  "\n" \
                  "*/balance:* `Show account balance per currency`\n" \
                  "*/help:* `This help message`\n" \
                  "*/version:* `Show version`"

        self._send_msg(message, bot=bot)

    @authorized_only
    def _version(self, bot: Bot, update: Update) -> None:
        """
        Handler for /version.
        Show version information
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        self._send_msg('*Version:* `{}`'.format(__version__), bot=bot)

    def _send_msg(self,
                  msg: str,
                  bot: Bot = None,
                  parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
        """
        Send given markdown message
        :param msg: message
        :param bot: alternative bot
        :param parse_mode: telegram parse mode
        :return: None
        """
        bot = bot or self._updater.bot

        keyboard = [['/daily', '/profit', '/balance'],
                    ['/status', '/status table', '/performance'],
                    ['/count', '/start', '/stop', '/help']]

        reply_markup = ReplyKeyboardMarkup(keyboard)

        try:
            try:
                bot.send_message(self._config['telegram']['chat_id'],
                                 text=msg,
                                 parse_mode=parse_mode,
                                 reply_markup=reply_markup)
            except NetworkError as network_err:
                # Sometimes the telegram server resets the current connection,
                # if this is the case we send the message again.
                logger.warning(
                    'Telegram NetworkError: %s! Trying one more time.',
                    network_err.message)
                bot.send_message(self._config['telegram']['chat_id'],
                                 text=msg,
                                 parse_mode=parse_mode,
                                 reply_markup=reply_markup)
        except TelegramError as telegram_err:
            logger.warning('TelegramError: %s! Giving up on that message.',
                           telegram_err.message)
Beispiel #7
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]) -> 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()

    def _init_modules(self) -> None:
        """
        Initializes all modules and updates the config
        :return: None
        """
        # Initialize all modules

        persistence.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 cleanup(self) -> None:
        """
        Cleanup pending resources on an already stopped bot
        :return: None
        """
        logger.info('Cleaning up modules ...')
        self.rpc.cleanup()
        persistence.cleanup()

    def worker(self, old_state: 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(f'*Status:* `{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 TemporaryError as error:
            logger.warning('%s, retrying in 30 seconds...', error)
            time.sleep(constants.RETRY_TIMEOUT)
        except OperationalException:
            tb = traceback.format_exc()
            hint = 'Issue `/start` if you think it is safe to restart.'
            self.rpc.send_msg(
                f'*Status:* OperationalException:\n```\n{tb}```{hint}')
            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 = 'quoteVolume') -> List[str]:
        """
        Updates the whitelist with with a dynamically generated list
        :param base_currency: base currency as str
        :param key: sort key (defaults to 'quoteVolume')
        :return: List of pairs
        """

        if not self.exchange.exchange_has('fetchTickers'):
            raise OperationalException(
                'Exchange does not support dynamic whitelist.'
                'Please edit your config and restart the bot')

        tickers = self.exchange.get_tickers()
        # check length so that we make sure that '/' is actually in the string
        tickers = [
            v for k, v in tickers.items()
            if len(k.split('/')) == 2 and k.split('/')[1] == base_currency
        ]

        sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key])
        pairs = [s['symbol'] for s in sorted_tickers]
        return pairs

    def _refresh_whitelist(self, whitelist: List[str]) -> List[str]:
        """
        Check available markets 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
        markets = self.exchange.get_markets()

        markets = [
            m for m in markets if m['quote'] == self.config['stake_currency']
        ]
        known_pairs = set()
        for market in markets:
            pair = market['symbol']
            # 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 market['active']:
                sanitized_whitelist.remove(pair)
                logger.info(
                    'Ignoring %s from whitelist. Market is not active.', pair)

        # 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 _get_trade_stake_amount(self) -> Optional[float]:
        stake_amount = self.config['stake_amount']
        avaliable_amount = self.exchange.get_balance(
            self.config['stake_currency'])

        if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
            open_trades = len(
                Trade.query.filter(Trade.is_open.is_(True)).all())
            if open_trades >= self.config['max_open_trades']:
                logger.warning(
                    'Can\'t open a new trade: max number of trades is reached')
                return None
            return avaliable_amount / (self.config['max_open_trades'] -
                                       open_trades)

        # Check if stake_amount is fulfilled
        if avaliable_amount < stake_amount:
            raise DependencyException(
                'Available balance(%f %s) is lower than stake amount(%f %s)' %
                (avaliable_amount, self.config['stake_currency'], stake_amount,
                 self.config['stake_currency']))

        return stake_amount

    def _get_min_pair_stake_amount(self, pair: str,
                                   price: float) -> Optional[float]:
        markets = self.exchange.get_markets()
        markets = [m for m in markets if m['symbol'] == pair]
        if not markets:
            raise ValueError(
                f'Can\'t get market information for symbol {pair}')

        market = markets[0]

        if 'limits' not in market:
            return None

        min_stake_amounts = []
        if 'cost' in market['limits'] and 'min' in market['limits']['cost']:
            min_stake_amounts.append(market['limits']['cost']['min'])

        if 'amount' in market['limits'] and 'min' in market['limits']['amount']:
            min_stake_amounts.append(market['limits']['amount']['min'] * price)

        if not min_stake_amounts:
            return None

        amount_reserve_percent = 1 - 0.05  # reserve 5% + stoploss
        if self.analyze.get_stoploss() is not None:
            amount_reserve_percent += self.analyze.get_stoploss()
        # it should not be more than 50%
        amount_reserve_percent = max(amount_reserve_percent, 0.5)
        return min(min_stake_amounts) / amount_reserve_percent

    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
        :return: True if a trade object has been created and persisted, False otherwise
        """
        interval = self.analyze.get_ticker_interval()
        stake_amount = self._get_trade_stake_amount()

        if not stake_amount:
            return False
        stake_currency = self.config['stake_currency']
        fiat_currency = self.config['fiat_display_currency']
        exc_name = self.exchange.name

        logger.info(
            'Checking buy signals to create a new trade with stake_amount: %f ...',
            stake_amount)
        whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])

        # 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 buy signals
        for _pair in whitelist:
            (buy, sell) = self.analyze.get_signal(self.exchange, _pair,
                                                  interval)
            if buy and not sell:
                pair = _pair
                break
        else:
            return False
        pair_s = pair.replace('_', '/')
        pair_url = self.exchange.get_pair_detail_url(pair)

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

        min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
        if min_stake_amount is not None and min_stake_amount > stake_amount:
            logger.warning(
                f'Can\'t open a new trade for {pair_s}: stake amount'
                f' is too small ({stake_amount} < {min_stake_amount})')
            return False

        amount = stake_amount / buy_limit

        order_id = self.exchange.buy(pair, buy_limit, amount)['id']

        stake_amount_fiat = self.fiat_converter.convert_amount(
            stake_amount, stake_currency, fiat_currency)

        # Create trade entity and return
        self.rpc.send_msg(f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
with limit `{buy_limit:.8f} ({stake_amount:.6f} \
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`""")
        # 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(pair=pair,
                      stake_amount=stake_amount,
                      amount=amount,
                      fee_open=fee,
                      fee_close=fee,
                      open_rate=buy_limit,
                      open_rate_requested=buy_limit,
                      open_date=datetime.utcnow(),
                      exchange=self.exchange.id,
                      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
        """
        try:
            # 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)
                order = self.exchange.get_order(trade.open_order_id,
                                                trade.pair)
                # Try update amount (binance-fix)
                try:
                    new_amount = self.get_real_amount(trade, order)
                    if order['amount'] != new_amount:
                        order['amount'] = new_amount
                        # Fee was applied, so set to 0
                        trade.fee_open = 0

                except OperationalException as exception:
                    logger.warning("could not update trade amount: %s",
                                   exception)

                trade.update(order)

            if trade.is_open and trade.open_order_id is None:
                # Check if we can sell our current pair
                return self.handle_trade(trade)
        except DependencyException as exception:
            logger.warning('Unable to sell trade: %s', exception)
        return False

    def get_real_amount(self, trade: Trade, order: Dict) -> float:
        """
        Get real amount for the trade
        Necessary for self.exchanges which charge fees in base currency (e.g. binance)
        """
        order_amount = order['amount']
        # Only run for closed orders
        if trade.fee_open == 0 or order['status'] == 'open':
            return order_amount

        # use fee from order-dict if possible
        if 'fee' in order and order['fee'] and (order['fee'].keys() >=
                                                {'currency', 'cost'}):
            if trade.pair.startswith(order['fee']['currency']):
                new_amount = order_amount - order['fee']['cost']
                logger.info(
                    "Applying fee on amount for %s (from %s to %s) from Order",
                    trade, order['amount'], new_amount)
                return new_amount

        # Fallback to Trades
        trades = self.exchange.get_trades_for_order(trade.open_order_id,
                                                    trade.pair,
                                                    trade.open_date)

        if len(trades) == 0:
            logger.info(
                "Applying fee on amount for %s failed: myTrade-Dict empty found",
                trade)
            return order_amount
        amount = 0
        fee_abs = 0
        for exectrade in trades:
            amount += exectrade['amount']
            if "fee" in exectrade and (exectrade['fee'].keys() >=
                                       {'currency', 'cost'}):
                # only applies if fee is in quote currency!
                if trade.pair.startswith(exectrade['fee']['currency']):
                    fee_abs += exectrade['fee']['cost']

        if amount != order_amount:
            logger.warning(
                f"amount {amount} does not match amount {trade.amount}")
            raise OperationalException("Half bought? Amounts don't match")
        real_amount = amount - fee_abs
        if fee_abs != 0:
            logger.info(f"""Applying fee on amount for {trade} \
(from {order_amount} to {real_amount}) from Trades""")
        return real_amount

    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(f'attempt to handle closed trade: {trade}')

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

        (buy, sell) = (False, False)
        experimental = self.config.get('experimental', {})
        if experimental.get('use_sell_signal') or experimental.get(
                'ignore_roi_if_buy_signal'):
            (buy, sell) = self.analyze.get_signal(
                self.exchange, 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
        logger.info(
            'Found no sell signals for whitelisted currencies. Trying again..')
        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:
                # FIXME: Somehow the query above returns results
                # where the open_order_id is in fact None.
                # This is probably because the record got
                # updated via /forcesell in a different thread.
                if not trade.open_order_id:
                    continue
                order = self.exchange.get_order(trade.open_order_id,
                                                trade.pair)
            except requests.exceptions.RequestException:
                logger.info('Cannot query order for %s due to %s', trade,
                            traceback.format_exc())
                continue
            ordertime = arrow.get(order['datetime']).datetime

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

            if order['side'] == 'buy' and ordertime < timeoutthreashold:
                self.handle_timedout_limit_buy(trade, order)
            elif order['side'] == '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
        """
        pair_s = trade.pair.replace('_', '/')
        self.exchange.cancel_order(trade.open_order_id, trade.pair)
        if order['remaining'] == order['amount']:
            # if trade is not partially completed, just delete the trade
            Trade.session.delete(trade)
            Trade.session.flush()
            logger.info('Buy order timeout for %s.', trade)
            self.rpc.send_msg(
                f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
            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(
            f'*Timeout:* Remaining buy order for {pair_s} cancelled')
        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
        """
        pair_s = trade.pair.replace('_', '/')
        if order['remaining'] == order['amount']:
            # if trade is not partially completed, just cancel the trade
            self.exchange.cancel_order(trade.open_order_id, trade.pair)
            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(
                f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
            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
        """
        exc = trade.exchange
        pair = trade.pair
        # Execute sell and update trade record
        order_id = self.exchange.sell(str(trade.pair), limit,
                                      trade.amount)['id']
        trade.open_order_id = order_id
        trade.close_rate_requested = limit

        fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
        profit_trade = trade.calc_profit(rate=limit)
        current_rate = self.exchange.get_ticker(trade.pair)['bid']
        profit = trade.calc_profit_percent(limit)
        pair_url = self.exchange.get_pair_detail_url(trade.pair)
        gain = "profit" if fmt_exp_profit > 0 else "loss"

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

        # For regular case, when the configuration exists
        if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
            stake = self.config['stake_currency']
            fiat = self.config['fiat_display_currency']
            fiat_converter = CryptoToFiatConverter()
            profit_fiat = fiat_converter.convert_amount(
                profit_trade, stake, fiat)
            message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
                       f'` / {profit_fiat:.3f} {fiat})`'\
                       ''
        # 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()
Beispiel #8
0
def create_trade(stake_amount: float) -> 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
    :return: True if a trade object has been created and persisted, False otherwise
    """
    logger.info(
        'Checking buy signals to create a new trade with stake_amount: %f ...',
        stake_amount
    )
    whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
    # Check if stake_amount is fulfilled
    if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
        raise DependencyException(
            'stake amount is not fulfilled (currency={})'.format(_CONF['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 pair in whitelist')

    # Pick pair based on StochRSI buy signals
    for _pair in whitelist:
        if get_signal(_pair, SignalType.BUY):
            pair = _pair
            break
    else:
        return False

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

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

    fiat_converter = CryptoToFiatConverter()
    stake_amount_fiat = fiat_converter.convert_amount(
        stake_amount,
        _CONF['stake_currency'],
        _CONF['fiat_display_currency']
    )

    # Create trade entity and return
    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, _CONF['stake_currency'],
        stake_amount_fiat, _CONF['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
Beispiel #9
0
def create_trade(stake_amount: float) -> 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
    :return: True if a trade object has been created and persisted, False otherwise
    """
    logger.info(
        'Checking buy signals to create a new trade with stake_amount: %f ...',
        stake_amount)
    whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
    # Check if stake_amount is fulfilled
    if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
        raise DependencyException(
            'stake amount is not fulfilled (currency={})'.format(
                _CONF['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 pair in whitelist')

    # Pick pair based on StochRSI buy signals
    for _pair in whitelist:
        if get_signal(_pair, SignalType.BUY):
            pair = _pair
            break
    else:
        return False

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

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

    fiat_converter = CryptoToFiatConverter()
    stake_amount_fiat = fiat_converter.convert_amount(
        stake_amount, _CONF['stake_currency'], _CONF['fiat_display_currency'])

    # Create trade entity and return
    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,
               _CONF['stake_currency'], stake_amount_fiat,
               _CONF['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