Beispiel #1
0
def test_fiat_invalid_response(mocker, caplog):
    # Because CryptoToFiatConverter is a Singleton we reset the listings
    listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}")
    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.Market',
        listings=listmock,
    )
    # with pytest.raises(RequestEsxception):
    fiat_convert = CryptoToFiatConverter()
    fiat_convert._cryptomap = {}
    fiat_convert._load_cryptomap()

    length_cryptomap = len(fiat_convert._cryptomap)
    assert length_cryptomap == 0
    assert log_has(
        'Could not load FIAT Cryptocurrency map for the following problem: TypeError',
        caplog.record_tuples)
Beispiel #2
0
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
        get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}),
    )
    mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        fetch_ticker=ticker,
        get_fee=fee,
    )
    del default_conf['fiat_display_currency']
    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    patch_get_signal(freqtradebot)
    rpc = RPC(freqtradebot)

    freqtradebot.state = State.RUNNING
    with pytest.raises(RPCException, match=r'.*no active trade*'):
        rpc._rpc_status_table(default_conf['stake_currency'], 'USD')

    freqtradebot.enter_positions()

    result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
    assert "Since" in headers
    assert "Pair" in headers
    assert 'instantly' == result[0][2]
    assert 'ETH/BTC' in result[0][1]
    assert '-0.41%' == result[0][3]
    assert isnan(fiat_profit_sum)
    # Test with fiatconvert

    rpc._fiat_converter = CryptoToFiatConverter()
    result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
    assert "Since" in headers
    assert "Pair" in headers
    assert len(result[0]) == 4
    assert 'instantly' == result[0][2]
    assert 'ETH/BTC' in result[0][1]
    assert '-0.41% (-0.06)' == result[0][3]
    assert '-0.06' == f'{fiat_profit_sum:.2f}'

    rpc._config['position_adjustment_enable'] = True
    rpc._config['max_entry_position_adjustment'] = 3
    result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
    assert "# Entries" in headers
    assert len(result[0]) == 5
    # 4th column should be 1/4 - as 1 order filled (a total of 4 is possible)
    # 3 on top of the initial one.
    assert result[0][4] == '1/4'

    mocker.patch('freqtrade.exchange.Exchange.get_rate',
                 MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
    result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
    assert 'instantly' == result[0][2]
    assert 'ETH/BTC' in result[0][1]
    assert 'nan%' == result[0][3]
    assert isnan(fiat_profit_sum)
Beispiel #3
0
def test_fiat_convert_get_price(mocker):
    find_price = mocker.patch(
        'freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
        return_value=28000.0)

    fiat_convert = CryptoToFiatConverter()

    with pytest.raises(ValueError,
                       match=r'The fiat us dollar is not supported.'):
        fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='US Dollar')

    # Check the value return by the method
    pair_len = len(fiat_convert._pair_price)
    assert pair_len == 0
    assert fiat_convert.get_price(crypto_symbol='btc',
                                  fiat_symbol='usd') == 28000.0
    assert fiat_convert._pair_price['btc/usd'] == 28000.0
    assert len(fiat_convert._pair_price) == 1
    assert find_price.call_count == 1

    # Verify the cached is used
    fiat_convert._pair_price['btc/usd'] = 9867.543
    assert fiat_convert.get_price(crypto_symbol='btc',
                                  fiat_symbol='usd') == 9867.543
    assert find_price.call_count == 1
def test_fiat_convert_get_price(mocker):
    mocker.patch(
        'freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
        return_value=28000.0)

    fiat_convert = CryptoToFiatConverter()

    with pytest.raises(ValueError,
                       match=r'The fiat us dollar is not supported.'):
        fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='US Dollar')

    # Check the value return by the method
    pair_len = len(fiat_convert._pairs)
    assert pair_len == 0
    assert fiat_convert.get_price(crypto_symbol='btc',
                                  fiat_symbol='usd') == 28000.0
    assert fiat_convert._pairs[0].crypto_symbol == 'btc'
    assert fiat_convert._pairs[0].fiat_symbol == 'usd'
    assert fiat_convert._pairs[0].price == 28000.0
    assert fiat_convert._pairs[0]._expiration != 0
    assert len(fiat_convert._pairs) == 1

    # Verify the cached is used
    fiat_convert._pairs[0].price = 9867.543
    expiration = fiat_convert._pairs[0]._expiration
    assert fiat_convert.get_price(crypto_symbol='btc',
                                  fiat_symbol='usd') == 9867.543
    assert fiat_convert._pairs[0]._expiration == expiration

    # Verify the cache expiration
    expiration = time.time() - 2 * 60 * 60
    fiat_convert._pairs[0]._expiration = expiration
    assert fiat_convert.get_price(crypto_symbol='btc',
                                  fiat_symbol='usd') == 28000.0
    assert fiat_convert._pairs[0]._expiration is not expiration
def test_convert_amount(mocker):
    mocker.patch('freqtrade.rpc.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

    result = fiat_convert.convert_amount(crypto_amount="1.23",
                                         crypto_symbol="BTC",
                                         fiat_symbol="BTC")
    assert result == 1.23
Beispiel #6
0
def test_fiat_too_many_requests_response(mocker, caplog):
    # Because CryptoToFiatConverter is a Singleton we reset the listings
    req_exception = "429 Too Many Requests"
    listmock = MagicMock(return_value="{}", side_effect=RequestException(req_exception))
    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
        get_coins_list=listmock,
    )
    # with pytest.raises(RequestEsxception):
    fiat_convert = CryptoToFiatConverter()
    fiat_convert._coinlistings = {}
    fiat_convert._load_cryptomap()

    assert len(fiat_convert._coinlistings) == 0
    assert fiat_convert._backoff > datetime.datetime.now().timestamp()
    assert log_has(
        'Too many requests for Coingecko API, backing off and trying again later.',
        caplog
    )
Beispiel #7
0
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
        get_price=MagicMock(return_value={'bitcoin': {
            'usd': 15000.0
        }}),
    )
    mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
                 return_value=15000.0)
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        fetch_ticker=ticker,
        get_fee=fee,
    )
    del default_conf['fiat_display_currency']
    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    patch_get_signal(freqtradebot, (True, False, None))
    rpc = RPC(freqtradebot)

    freqtradebot.state = State.RUNNING
    with pytest.raises(RPCException, match=r'.*no active trade*'):
        rpc._rpc_status_table(default_conf['stake_currency'], 'USD')

    freqtradebot.enter_positions()

    result, headers, fiat_profit_sum = rpc._rpc_status_table(
        default_conf['stake_currency'], 'USD')
    assert "Since" in headers
    assert "Pair" in headers
    assert 'instantly' == result[0][2]
    assert 'ETH/BTC' in result[0][1]
    assert '-0.41%' == result[0][3]
    assert isnan(fiat_profit_sum)
    # Test with fiatconvert

    rpc._fiat_converter = CryptoToFiatConverter()
    result, headers, fiat_profit_sum = rpc._rpc_status_table(
        default_conf['stake_currency'], 'USD')
    assert "Since" in headers
    assert "Pair" in headers
    assert 'instantly' == result[0][2]
    assert 'ETH/BTC' in result[0][1]
    assert '-0.41% (-0.06)' == result[0][3]
    assert '-0.06' == f'{fiat_profit_sum:.2f}'

    mocker.patch(
        'freqtrade.exchange.Exchange.get_rate',
        MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
    result, headers, fiat_profit_sum = rpc._rpc_status_table(
        default_conf['stake_currency'], 'USD')
    assert 'instantly' == result[0][2]
    assert 'ETH/BTC' in result[0][1]
    assert 'nan%' == result[0][3]
    assert isnan(fiat_profit_sum)
Beispiel #8
0
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.Market',
        ticker=MagicMock(return_value={'price_usd': 15000.0}),
    )
    mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
                 return_value=15000.0)
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        get_ticker=ticker,
        get_fee=fee,
    )

    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    patch_get_signal(freqtradebot, (True, False))
    rpc = RPC(freqtradebot)

    freqtradebot.state = State.RUNNING
    with pytest.raises(RPCException, match=r'.*no active order*'):
        rpc._rpc_status_table(default_conf['stake_currency'], 'USD')

    freqtradebot.create_trades()

    result, headers = rpc._rpc_status_table(default_conf['stake_currency'],
                                            'USD')
    assert "Since" in headers
    assert "Pair" in headers
    assert 'instantly' == result[0][2]
    assert 'ETH/BTC' == result[0][1]
    assert '-0.59%' == result[0][3]
    # Test with fiatconvert

    rpc._fiat_converter = CryptoToFiatConverter()
    result, headers = rpc._rpc_status_table(default_conf['stake_currency'],
                                            'USD')
    assert "Since" in headers
    assert "Pair" in headers
    assert 'instantly' == result[0][2]
    assert 'ETH/BTC' == result[0][1]
    assert '-0.59% (-0.09)' == result[0][3]

    mocker.patch(
        'freqtrade.exchange.Exchange.get_ticker',
        MagicMock(
            side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
    # invalidate ticker cache
    rpc._freqtrade.exchange._cached_ticker = {}
    result, headers = rpc._rpc_status_table(default_conf['stake_currency'],
                                            'USD')
    assert 'instantly' == result[0][2]
    assert 'ETH/BTC' == result[0][1]
    assert 'nan%' == result[0][3]
Beispiel #9
0
def test_fiat_convert_add_pair(mocker):
    patch_coinmarketcap(mocker)

    fiat_convert = CryptoToFiatConverter()

    pair_len = len(fiat_convert._pairs)
    assert pair_len == 0

    fiat_convert._add_pair(crypto_symbol='btc',
                           fiat_symbol='usd',
                           price=12345.0)
    pair_len = len(fiat_convert._pairs)
    assert pair_len == 1
    assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
    assert fiat_convert._pairs[0].fiat_symbol == 'USD'
    assert fiat_convert._pairs[0].price == 12345.0

    fiat_convert._add_pair(crypto_symbol='btc',
                           fiat_symbol='Eur',
                           price=13000.2)
    pair_len = len(fiat_convert._pairs)
    assert pair_len == 2
    assert fiat_convert._pairs[1].crypto_symbol == 'BTC'
    assert fiat_convert._pairs[1].fiat_symbol == 'EUR'
    assert fiat_convert._pairs[1].price == 13000.2
Beispiel #10
0
def test_fiat_convert_find_price(mocker):
    fiat_convert = CryptoToFiatConverter()

    fiat_convert._cryptomap = {}
    fiat_convert._backoff = 0
    mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap',
                 return_value=None)
    assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 0.0

    with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
        fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')

    assert fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD') == 0.0

    mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
                 return_value=12345.0)
    assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
    assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0

    mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
                 return_value=13000.2)
    assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
Beispiel #11
0
def test_rpc_daily_profit(default_conf, update, ticker, fee, limit_buy_order,
                          limit_sell_order, markets, mocker) -> None:
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple('freqtrade.exchange.Exchange',
                          fetch_ticker=ticker,
                          get_fee=fee,
                          markets=PropertyMock(return_value=markets))

    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    patch_get_signal(freqtradebot)
    stake_currency = default_conf['stake_currency']
    fiat_display_currency = default_conf['fiat_display_currency']

    rpc = RPC(freqtradebot)
    rpc._fiat_converter = CryptoToFiatConverter()
    # Create some test data
    freqtradebot.enter_positions()
    trade = Trade.query.first()
    assert trade

    # Simulate buy & sell
    oobj = Order.parse_from_ccxt_object(limit_buy_order,
                                        limit_buy_order['symbol'], 'buy')
    trade.update_trade(oobj)
    oobj = Order.parse_from_ccxt_object(limit_sell_order,
                                        limit_sell_order['symbol'], 'sell')
    trade.update_trade(oobj)
    trade.close_date = datetime.utcnow()
    trade.is_open = False

    # Try valid data
    update.message.text = '/daily 2'
    days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
    assert len(days['data']) == 7
    assert days['stake_currency'] == default_conf['stake_currency']
    assert days['fiat_display_currency'] == default_conf[
        'fiat_display_currency']
    for day in days['data']:
        # [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
        assert (day['abs_profit'] == 0.0 or day['abs_profit'] == 0.00006217)

        assert (day['fiat_value'] == 0.0 or day['fiat_value'] == 0.76748865)
    # ensure first day is current date
    assert str(days['data'][0]['date']) == str(datetime.utcnow().date())

    # Try invalid data
    with pytest.raises(RPCException,
                       match=r'.*must be an integer greater than 0*'):
        rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
Beispiel #12
0
def test_fiat_multiple_coins(mocker, caplog):
    fiat_convert = CryptoToFiatConverter()
    fiat_convert._coinlistings = [
        {
            'id': 'helium',
            'symbol': 'hnt',
            'name': 'Helium'
        },
        {
            'id': 'hymnode',
            'symbol': 'hnt',
            'name': 'Hymnode'
        },
        {
            'id': 'bitcoin',
            'symbol': 'btc',
            'name': 'Bitcoin'
        },
    ]

    assert fiat_convert._get_gekko_id('btc') == 'bitcoin'
    assert fiat_convert._get_gekko_id('hnt') is None

    assert log_has('Found multiple mappings in goingekko for hnt.', caplog)
Beispiel #13
0
def test_rpc_balance_handle(default_conf, mocker):
    mock_balance = {
        'BTC': {
            'free': 10.0,
            'total': 12.0,
            'used': 2.0,
        },
        'ETH': {
            'free': 1.0,
            'total': 5.0,
            'used': 4.0,
        }
    }
    # ETH will be skipped due to mocked Error below

    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.Market',
        ticker=MagicMock(return_value={'price_usd': 15000.0}),
    )
    patch_coinmarketcap(mocker)
    patch_exchange(mocker)
    mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
                 return_value=15000.0)
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        get_balances=MagicMock(return_value=mock_balance),
        get_ticker=MagicMock(
            side_effect=TemporaryError('Could not load ticker due to xxx')))

    freqtradebot = FreqtradeBot(default_conf)
    patch_get_signal(freqtradebot, (True, False))
    rpc = RPC(freqtradebot)
    rpc._fiat_converter = CryptoToFiatConverter()

    result = rpc._rpc_balance(default_conf['fiat_display_currency'])
    assert prec_satoshi(result['total'], 12)
    assert prec_satoshi(result['value'], 180000)
    assert 'USD' == result['symbol']
    assert result['currencies'] == [{
        'currency': 'BTC',
        'available': 10.0,
        'balance': 12.0,
        'pending': 2.0,
        'est_btc': 12.0,
    }]
    assert result['total'] == 12.0
Beispiel #14
0
def test_rpc_daily_profit(default_conf, update, ticker, fee,
                          limit_buy_order, limit_sell_order, markets, mocker) -> None:
    patch_exchange(mocker)
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        get_ticker=ticker,
        get_fee=fee,
        markets=PropertyMock(return_value=markets)
    )

    freqtradebot = FreqtradeBot(default_conf)
    patch_get_signal(freqtradebot, (True, False))
    stake_currency = default_conf['stake_currency']
    fiat_display_currency = default_conf['fiat_display_currency']

    rpc = RPC(freqtradebot)
    rpc._fiat_converter = CryptoToFiatConverter()
    # Create some test data
    freqtradebot.create_trades()
    trade = Trade.query.first()
    assert trade

    # Simulate buy & sell
    trade.update(limit_buy_order)
    trade.update(limit_sell_order)
    trade.close_date = datetime.utcnow()
    trade.is_open = False

    # Try valid data
    update.message.text = '/daily 2'
    days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
    assert len(days) == 7
    for day in days:
        # [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
        assert (day[1] == '0.00000000 BTC' or
                day[1] == '0.00006217 BTC')

        assert (day[2] == '0.000 USD' or
                day[2] == '0.933 USD')
    # ensure first day is current date
    assert str(days[0][0]) == str(datetime.utcnow().date())

    # Try invalid data
    with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
        rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
Beispiel #15
0
def test_rpc_trade_history(mocker, default_conf, markets, fee):
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple('freqtrade.exchange.Exchange',
                          markets=PropertyMock(return_value=markets))

    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    create_mock_trades(fee)
    rpc = RPC(freqtradebot)
    rpc._fiat_converter = CryptoToFiatConverter()
    trades = rpc._rpc_trade_history(2)
    assert len(trades['trades']) == 2
    assert trades['trades_count'] == 2
    assert isinstance(trades['trades'][0], dict)
    assert isinstance(trades['trades'][1], dict)

    trades = rpc._rpc_trade_history(0)
    assert len(trades['trades']) == 2
    assert trades['trades_count'] == 2
    # The first closed trade is for ETC ... sorting is descending
    assert trades['trades'][-1]['pair'] == 'ETC/BTC'
    assert trades['trades'][0]['pair'] == 'XRP/BTC'
    def __init__(self, freqtrade) -> None:
        """
        Init the api server, and init the super class RPC
        :param freqtrade: Instance of a freqtrade bot
        :return: None
        """
        super().__init__(freqtrade)

        self._config = freqtrade.config
        self.app = Flask(__name__)
        self._cors = CORS(self.app,
                          resources={
                              r"/api/*": {
                                  "supports_credentials":
                                  True,
                                  "origins":
                                  self._config['api_server'].get(
                                      'CORS_origins', [])
                              }
                          })

        # Setup the Flask-JWT-Extended extension
        self.app.config['JWT_SECRET_KEY'] = self._config['api_server'].get(
            'jwt_secret_key', 'super-secret')

        self.jwt = JWTManager(self.app)
        self.app.json_encoder = FTJSONEncoder

        self.app.teardown_appcontext(shutdown_session)

        # Register application handling
        self.register_rest_rpc_urls()

        if self._config.get('fiat_display_currency', None):
            self._fiat_converter = CryptoToFiatConverter()

        thread = threading.Thread(target=self.run, daemon=True)
        thread.start()
Beispiel #17
0
def test_rpc_balance_handle_error(default_conf, mocker):
    mock_balance = {
        'BTC': {
            'free': 10.0,
            'total': 12.0,
            'used': 2.0,
        },
        'ETH': {
            'free': 1.0,
            'total': 5.0,
            'used': 4.0,
        }
    }
    # ETH will be skipped due to mocked Error below

    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
        get_price=MagicMock(return_value={'bitcoin': {
            'usd': 15000.0
        }}),
    )
    mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
                 return_value=15000.0)
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        get_balances=MagicMock(return_value=mock_balance),
        get_tickers=MagicMock(
            side_effect=TemporaryError('Could not load ticker due to xxx')))

    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    patch_get_signal(freqtradebot, (True, False))
    rpc = RPC(freqtradebot)
    rpc._fiat_converter = CryptoToFiatConverter()
    with pytest.raises(RPCException, match="Error getting current tickers."):
        rpc._rpc_balance(default_conf['stake_currency'],
                         default_conf['fiat_display_currency'])
def test_fiat_convert_add_pair(mocker):

    fiat_convert = CryptoToFiatConverter()

    pair_len = len(fiat_convert._pairs)
    assert pair_len == 0

    fiat_convert._add_pair(crypto_symbol='btc',
                           fiat_symbol='usd',
                           price=12345.0)
    pair_len = len(fiat_convert._pairs)
    assert pair_len == 1
    assert fiat_convert._pairs[0].crypto_symbol == 'btc'
    assert fiat_convert._pairs[0].fiat_symbol == 'usd'
    assert fiat_convert._pairs[0].price == 12345.0

    fiat_convert._add_pair(crypto_symbol='btc',
                           fiat_symbol='Eur',
                           price=13000.2)
    pair_len = len(fiat_convert._pairs)
    assert pair_len == 2
    assert fiat_convert._pairs[1].crypto_symbol == 'btc'
    assert fiat_convert._pairs[1].fiat_symbol == 'eur'
    assert fiat_convert._pairs[1].price == 13000.2
Beispiel #19
0
def test_rpc_balance_handle(default_conf, mocker, tickers):
    mock_balance = {
        'BTC': {
            'free': 10.0,
            'total': 12.0,
            'used': 2.0,
        },
        'ETH': {
            'free': 1.0,
            'total': 5.0,
            'used': 4.0,
        },
        'USDT': {
            'free': 5.0,
            'total': 10.0,
            'used': 5.0,
        }
    }

    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
        get_price=MagicMock(return_value={'bitcoin': {
            'usd': 15000.0
        }}),
    )
    mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
                 return_value=15000.0)
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple('freqtrade.exchange.Exchange',
                          get_balances=MagicMock(return_value=mock_balance),
                          get_tickers=tickers,
                          get_valid_pair_combination=MagicMock(
                              side_effect=lambda a, b: f"{b}/{a}"
                              if a == "USDT" else f"{a}/{b}"))
    default_conf['dry_run'] = False
    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    patch_get_signal(freqtradebot, (True, False))
    rpc = RPC(freqtradebot)
    rpc._fiat_converter = CryptoToFiatConverter()

    result = rpc._rpc_balance(default_conf['stake_currency'],
                              default_conf['fiat_display_currency'])
    assert prec_satoshi(result['total'], 12.309096315)
    assert prec_satoshi(result['value'], 184636.44472997)
    assert 'USD' == result['symbol']
    assert result['currencies'] == [{
        'currency': 'BTC',
        'free': 10.0,
        'balance': 12.0,
        'used': 2.0,
        'est_stake': 12.0,
        'stake': 'BTC',
    }, {
        'free': 1.0,
        'balance': 5.0,
        'currency': 'ETH',
        'est_stake': 0.30794,
        'used': 4.0,
        'stake': 'BTC',
    }, {
        'free': 5.0,
        'balance': 10.0,
        'currency': 'USDT',
        'est_stake': 0.0011563153318162476,
        'used': 5.0,
        'stake': 'BTC',
    }]
    assert result['total'] == 12.309096315331816
Beispiel #20
0
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
                              limit_buy_order, limit_sell_order,
                              mocker) -> None:
    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
        get_price=MagicMock(return_value={'bitcoin': {
            'usd': 15000.0
        }}),
    )
    mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
                 return_value=15000.0)
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        fetch_ticker=ticker,
        get_fee=fee,
    )

    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    patch_get_signal(freqtradebot, (True, False))
    stake_currency = default_conf['stake_currency']
    fiat_display_currency = default_conf['fiat_display_currency']

    rpc = RPC(freqtradebot)
    rpc._fiat_converter = CryptoToFiatConverter()

    res = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
    assert res['trade_count'] == 0
    assert res['first_trade_date'] == ''
    assert res['first_trade_timestamp'] == 0
    assert res['latest_trade_date'] == ''
    assert res['latest_trade_timestamp'] == 0

    # Create some test data
    freqtradebot.enter_positions()
    trade = Trade.query.first()
    # Simulate fulfilled LIMIT_BUY order for trade
    trade.update(limit_buy_order)

    # Update the ticker with a market going up
    mocker.patch.multiple('freqtrade.exchange.Exchange',
                          fetch_ticker=ticker_sell_up)
    trade.update(limit_sell_order)
    trade.close_date = datetime.utcnow()
    trade.is_open = False

    freqtradebot.enter_positions()
    trade = Trade.query.first()
    # Simulate fulfilled LIMIT_BUY order for trade
    trade.update(limit_buy_order)

    # Update the ticker with a market going up
    mocker.patch.multiple('freqtrade.exchange.Exchange',
                          fetch_ticker=ticker_sell_up)
    trade.update(limit_sell_order)
    trade.close_date = datetime.utcnow()
    trade.is_open = False

    stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
    assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
    assert prec_satoshi(stats['profit_closed_percent'], 6.2)
    assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
    assert prec_satoshi(stats['profit_all_coin'], 5.802e-05)
    assert prec_satoshi(stats['profit_all_percent'], 2.89)
    assert prec_satoshi(stats['profit_all_fiat'], 0.8703)
    assert stats['trade_count'] == 2
    assert stats['first_trade_date'] == 'just now'
    assert stats['latest_trade_date'] == 'just now'
    assert stats['avg_duration'] == '0:00:00'
    assert stats['best_pair'] == 'ETH/BTC'
    assert prec_satoshi(stats['best_rate'], 6.2)

    # Test non-available pair
    mocker.patch(
        'freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
        MagicMock(
            side_effect=DependencyException("Pair 'ETH/BTC' not available")))
    stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
    assert stats['trade_count'] == 2
    assert stats['first_trade_date'] == 'just now'
    assert stats['latest_trade_date'] == 'just now'
    assert stats['avg_duration'] == '0:00:00'
    assert stats['best_pair'] == 'ETH/BTC'
    assert prec_satoshi(stats['best_rate'], 6.2)
    assert isnan(stats['profit_all_coin'])
Beispiel #21
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,
                                use_context=True)

        # 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('forcebuy', self._forcebuy),
            CommandHandler('performance', self._performance),
            CommandHandler('daily', self._daily),
            CommandHandler('count', self._count),
            CommandHandler('reload_conf', self._reload_conf),
            CommandHandler('stopbuy', self._stopbuy),
            CommandHandler('whitelist', self._whitelist),
            CommandHandler('blacklist', self._blacklist),
            CommandHandler('edge', self._edge),
            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}\n"
                       "at rate `{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}\n"
                       "*Rate:* `{limit:.8f}`\n"
                       "*Amount:* `{amount:.8f}`\n"
                       "*Open Rate:* `{open_rate:.8f}`\n"
                       "*Current Rate:* `{current_rate:.8f}`\n"
                       "*Sell Reason:* `{sell_reason}`\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, update: Update, context: CallbackContext) -> None:
        """
        Handler for /status.
        Returns the current TradeThread status
        :param bot: telegram bot
        :param update: message update
        :return: None
        """

        if 'table' in context.args:
            self._status_table(update, context)
            return

        try:
            results = self._rpc_trade_status()

            messages = []
            for r in results:
                lines = [
                    "*ID сделки:* `{trade_id}` `(since {open_date_hum})`",
                    "*Выбранная пара:* {pair}",
                    "*Количество:* `{amount} ({stake_amount} {base_currency})`",
                    "*Открытый рейт:* `{open_rate:.8f}`",
                    "*Закрытый рейт:* `{close_rate}`" if r['close_rate'] else "",
                    "*Настоящий рейт:* `{current_rate:.8f}`",
                    "*Профит на выходе:* `{close_profit}`" if r['close_profit'] else "",
                    "*Профит сейчас:* `{current_profit:.2f}%`",

                    # Adding initial stoploss only if it is different from stoploss
                    "*Initial Stoploss:* `{initial_stop_loss:.8f}` " +
                    ("`({initial_stop_loss_pct:.2f}%)`" if r['initial_stop_loss_pct'] else "")
                    if r['stop_loss'] != r['initial_stop_loss'] else "",

                    # Adding stoploss and stoploss percentage only if it is not None
                    "*Stoploss:* `{stop_loss:.8f}` " +
                    ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""),

                    "*Open Order:* `{open_order}`" if r['open_order'] else ""
                ]
                # Filter empty lines using list-comprehension
                messages.append("\n".join([l for l in lines if l]).format(**r))

            for msg in messages:
                self._send_msg(msg)

        except RPCException as e:
            self._send_msg(str(e))

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

    @authorized_only
    def _daily(self, update: Update, context: CallbackContext) -> 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(context.args[0])
        except (TypeError, ValueError, IndexError):
            timescale = 7
        try:
            stats = self._rpc_daily_profit(
                timescale,
                stake_cur,
                fiat_disp_cur
            )
            stats_tab = tabulate(stats,
                                 headers=[
                                     'Day',
                                     f'Profit {stake_cur}',
                                     f'Profit {fiat_disp_cur}',
                                     f'Trades'
                                 ],
                                 tablefmt='simple')
            message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
            self._send_msg(message, parse_mode=ParseMode.HTML)
        except RPCException as e:
            self._send_msg(str(e))

    @authorized_only
    def _profit(self, update: Update, context: CallbackContext) -> 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)
        except RPCException as e:
            self._send_msg(str(e))

    @authorized_only
    def _balance(self, update: Update, context: CallbackContext) -> None:
        """ Handler for /balance """
        try:
            result = self._rpc_balance(self._config.get('fiat_display_currency', ''))
            output = ''
            for currency in result['currencies']:
                if currency['est_btc'] > 0.0001:
                    curr_output = "*{currency}:*\n" \
                            "\t`Available: {free: .8f}`\n" \
                            "\t`Balance: {balance: .8f}`\n" \
                            "\t`Pending: {used: .8f}`\n" \
                            "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
                else:
                    curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency)

                # Handle overflowing messsage length
                if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
                    self._send_msg(output)
                    output = curr_output
                else:
                    output += curr_output

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

    @authorized_only
    def _start(self, update: Update, context: CallbackContext) -> 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))

    @authorized_only
    def _stop(self, update: Update, context: CallbackContext) -> 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))

    @authorized_only
    def _reload_conf(self, update: Update, context: CallbackContext) -> 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))

    @authorized_only
    def _stopbuy(self, update: Update, context: CallbackContext) -> None:
        """
        Handler for /stop_buy.
        Sets max_open_trades to 0 and gracefully sells all open trades
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        msg = self._rpc_stopbuy()
        self._send_msg('Status: `{status}`'.format(**msg))

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

        trade_id = context.args[0] if len(context.args) > 0 else None
        try:
            msg = self._rpc_forcesell(trade_id)
            self._send_msg('Forcesell Result: `{result}`'.format(**msg))

        except RPCException as e:
            self._send_msg(str(e))

    @authorized_only
    def _forcebuy(self, update: Update, context: CallbackContext) -> None:
        """
        Handler for /forcebuy <asset> <price>.
        Buys a pair trade at the given or current price
        :param bot: telegram bot
        :param update: message update
        :return: None
        """

        pair = context.args[0]
        price = float(context.args[1]) if len(context.args) > 1 else None
        try:
            self._rpc_forcebuy(pair, price)
        except RPCException as e:
            self._send_msg(str(e))

    @authorized_only
    def _performance(self, update: Update, context: CallbackContext) -> 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))

    @authorized_only
    def _count(self, update: Update, context: CallbackContext) -> None:
        """
        Handler for /count.
        Returns the number of trades running
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        try:
            counts = self._rpc_count()
            message = tabulate({k: [v] for k, v in counts.items()},
                               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))

    @authorized_only
    def _whitelist(self, update: Update, context: CallbackContext) -> None:
        """
        Handler for /whitelist
        Shows the currently active whitelist
        """
        try:
            whitelist = self._rpc_whitelist()

            message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n"
            message += f"`{', '.join(whitelist['whitelist'])}`"

            logger.debug(message)
            self._send_msg(message)
        except RPCException as e:
            self._send_msg(str(e))

    @authorized_only
    def _blacklist(self, update: Update, context: CallbackContext) -> None:
        """
        Handler for /blacklist
        Shows the currently active blacklist
        """
        try:

            blacklist = self._rpc_blacklist(context.args)

            message = f"Blacklist contains {blacklist['length']} pairs\n"
            message += f"`{', '.join(blacklist['blacklist'])}`"

            logger.debug(message)
            self._send_msg(message)
        except RPCException as e:
            self._send_msg(str(e))

    @authorized_only
    def _edge(self, update: Update, context: CallbackContext) -> None:
        """
        Handler for /edge
        Shows information related to Edge
        """
        try:
            edge_pairs = self._rpc_edge()
            edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple')
            message = f'<b>Edge only validated following pairs:</b>\n<pre>{edge_pairs_tab}</pre>'
            self._send_msg(message, parse_mode=ParseMode.HTML)
        except RPCException as e:
            self._send_msg(str(e))

    @authorized_only
    def _help(self, update: Update, context: CallbackContext) -> None:
        """
        Handler for /help.
        Show commands of the bot
        :param bot: telegram bot
        :param update: message update
        :return: None
        """
        forcebuy_text = "*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. " \
                        "Optionally takes a rate at which to buy.` \n"
        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" \
                  f"{forcebuy_text if self._config.get('forcebuy_enable', False) else '' }" \
                  "*/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" \
                  "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \
                  "*/reload_conf:* `Reload configuration file` \n" \
                  "*/whitelist:* `Show current whitelist` \n" \
                  "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \
                  "to the blacklist.` \n" \
                  "*/edge:* `Shows validated pairs by Edge if it is enabeld` \n" \
                  "*/help:* `This help message`\n" \
                  "*/version:* `Show version`"

        self._send_msg(message)

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

    def _send_msg(self, msg: str, 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
        """

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

        reply_markup = ReplyKeyboardMarkup(keyboard)

        try:
            try:
                self._updater.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
                )
                self._updater.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 #22
0
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
                              limit_buy_order, limit_sell_order, markets, mocker) -> None:
    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.Market',
        ticker=MagicMock(return_value={'price_usd': 15000.0}),
    )
    patch_exchange(mocker)
    mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        get_ticker=ticker,
        get_fee=fee,
        markets=PropertyMock(return_value=markets)
    )

    freqtradebot = FreqtradeBot(default_conf)
    patch_get_signal(freqtradebot, (True, False))
    stake_currency = default_conf['stake_currency']
    fiat_display_currency = default_conf['fiat_display_currency']

    rpc = RPC(freqtradebot)
    rpc._fiat_converter = CryptoToFiatConverter()

    with pytest.raises(RPCException, match=r'.*no closed trade*'):
        rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)

    # Create some test data
    freqtradebot.create_trades()
    trade = Trade.query.first()
    # Simulate fulfilled LIMIT_BUY order for trade
    trade.update(limit_buy_order)

    # Update the ticker with a market going up
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        get_ticker=ticker_sell_up
    )
    trade.update(limit_sell_order)
    trade.close_date = datetime.utcnow()
    trade.is_open = False

    freqtradebot.create_trades()
    trade = Trade.query.first()
    # Simulate fulfilled LIMIT_BUY order for trade
    trade.update(limit_buy_order)

    # Update the ticker with a market going up
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        get_ticker=ticker_sell_up
    )
    trade.update(limit_sell_order)
    trade.close_date = datetime.utcnow()
    trade.is_open = False

    stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
    assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
    assert prec_satoshi(stats['profit_closed_percent'], 6.2)
    assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
    assert prec_satoshi(stats['profit_all_coin'], 5.632e-05)
    assert prec_satoshi(stats['profit_all_percent'], 2.81)
    assert prec_satoshi(stats['profit_all_fiat'], 0.8448)
    assert stats['trade_count'] == 2
    assert stats['first_trade_date'] == 'just now'
    assert stats['latest_trade_date'] == 'just now'
    assert stats['avg_duration'] == '0:00:00'
    assert stats['best_pair'] == 'ETH/BTC'
    assert prec_satoshi(stats['best_rate'], 6.2)

    # Test non-available pair
    mocker.patch('freqtrade.exchange.Exchange.get_ticker',
                 MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
    # invalidate ticker cache
    rpc._freqtrade.exchange._cached_ticker = {}
    stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
    assert stats['trade_count'] == 2
    assert stats['first_trade_date'] == 'just now'
    assert stats['latest_trade_date'] == 'just now'
    assert stats['avg_duration'] == '0:00:00'
    assert stats['best_pair'] == 'ETH/BTC'
    assert prec_satoshi(stats['best_rate'], 6.2)
    assert isnan(stats['profit_all_coin'])
Beispiel #23
0
class RPC:
    """
    RPC class can be used to have extra feature, like bot data, and access to DB data
    """
    # Bind _fiat_converter if needed
    _fiat_converter: Optional[CryptoToFiatConverter] = None

    def __init__(self, freqtrade) -> None:
        """
        Initializes all enabled rpc modules
        :param freqtrade: Instance of a freqtrade bot
        :return: None
        """
        self._freqtrade = freqtrade
        self._config: Dict[str, Any] = freqtrade.config
        if self._config.get('fiat_display_currency', None):
            self._fiat_converter = CryptoToFiatConverter()

    @staticmethod
    def _rpc_show_config(config, botstate: Union[State, str],
                         strategy_version: Optional[str] = None) -> Dict[str, Any]:
        """
        Return a dict of config options.
        Explicitly does NOT return the full config to avoid leakage of sensitive
        information via rpc.
        """
        val = {
            'version': __version__,
            'strategy_version': strategy_version,
            'dry_run': config['dry_run'],
            'stake_currency': config['stake_currency'],
            'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
            'stake_amount': config['stake_amount'],
            'available_capital': config.get('available_capital'),
            'max_open_trades': (config['max_open_trades']
                                if config['max_open_trades'] != float('inf') else -1),
            'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
            'stoploss': config.get('stoploss'),
            'trailing_stop': config.get('trailing_stop'),
            'trailing_stop_positive': config.get('trailing_stop_positive'),
            'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
            'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
            'unfilledtimeout': config.get('unfilledtimeout'),
            'use_custom_stoploss': config.get('use_custom_stoploss'),
            'order_types': config.get('order_types'),
            'bot_name': config.get('bot_name', 'freqtrade'),
            'timeframe': config.get('timeframe'),
            'timeframe_ms': timeframe_to_msecs(config['timeframe']
                                               ) if 'timeframe' in config else 0,
            'timeframe_min': timeframe_to_minutes(config['timeframe']
                                                  ) if 'timeframe' in config else 0,
            'exchange': config['exchange']['name'],
            'strategy': config['strategy'],
            'forcebuy_enabled': config.get('forcebuy_enable', False),
            'ask_strategy': config.get('ask_strategy', {}),
            'bid_strategy': config.get('bid_strategy', {}),
            'state': str(botstate),
            'runmode': config['runmode'].value
        }
        return val

    def _rpc_trade_status(self, trade_ids: List[int] = []) -> List[Dict[str, Any]]:
        """
        Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
        a remotely exposed function
        """
        # Fetch open trades
        if trade_ids:
            trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
        else:
            trades = Trade.get_open_trades()

        if not trades:
            raise RPCException('no active trade')
        else:
            results = []
            for trade in trades:
                order = None
                if trade.open_order_id:
                    order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
                # calculate profit and send message to user
                if trade.is_open:
                    try:
                        current_rate = self._freqtrade.exchange.get_rate(
                            trade.pair, refresh=False, side="sell")
                    except (ExchangeError, PricingError):
                        current_rate = NAN
                else:
                    current_rate = trade.close_rate
                current_profit = trade.calc_profit_ratio(current_rate)
                current_profit_abs = trade.calc_profit(current_rate)
                current_profit_fiat: Optional[float] = None
                # Calculate fiat profit
                if self._fiat_converter:
                    current_profit_fiat = self._fiat_converter.convert_amount(
                        current_profit_abs,
                        self._freqtrade.config['stake_currency'],
                        self._freqtrade.config['fiat_display_currency']
                    )

                # Calculate guaranteed profit (in case of trailing stop)
                stoploss_entry_dist = trade.calc_profit(trade.stop_loss)
                stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss)
                # calculate distance to stoploss
                stoploss_current_dist = trade.stop_loss - current_rate
                stoploss_current_dist_ratio = stoploss_current_dist / current_rate

                trade_dict = trade.to_json()
                trade_dict.update(dict(
                    base_currency=self._freqtrade.config['stake_currency'],
                    close_profit=trade.close_profit if trade.close_profit is not None else None,
                    current_rate=current_rate,
                    current_profit=current_profit,  # Deprecated
                    current_profit_pct=round(current_profit * 100, 2),  # Deprecated
                    current_profit_abs=current_profit_abs,  # Deprecated
                    profit_ratio=current_profit,
                    profit_pct=round(current_profit * 100, 2),
                    profit_abs=current_profit_abs,
                    profit_fiat=current_profit_fiat,

                    stoploss_current_dist=stoploss_current_dist,
                    stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
                    stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
                    stoploss_entry_dist=stoploss_entry_dist,
                    stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
                    open_order='({} {} rem={:.8f})'.format(
                        order['type'], order['side'], order['remaining']
                    ) if order else None,
                ))
                results.append(trade_dict)
            return results

    def _rpc_status_table(self, stake_currency: str,
                          fiat_display_currency: str) -> Tuple[List, List, float]:
        trades = Trade.get_open_trades()
        if not trades:
            raise RPCException('no active trade')
        else:
            trades_list = []
            fiat_profit_sum = NAN
            for trade in trades:
                # calculate profit and send message to user
                try:
                    current_rate = self._freqtrade.exchange.get_rate(
                        trade.pair, refresh=False, side="sell")
                except (PricingError, ExchangeError):
                    current_rate = NAN
                trade_profit = trade.calc_profit(current_rate)
                profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
                if self._fiat_converter:
                    fiat_profit = self._fiat_converter.convert_amount(
                        trade_profit,
                        stake_currency,
                        fiat_display_currency
                    )
                    if fiat_profit and not isnan(fiat_profit):
                        profit_str += f" ({fiat_profit:.2f})"
                        fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \
                            else fiat_profit_sum + fiat_profit
                trades_list.append([
                    trade.id,
                    trade.pair + ('*' if (trade.open_order_id is not None
                                          and trade.close_rate_requested is None) else '')
                               + ('**' if (trade.close_rate_requested is not None) else ''),
                    shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
                    profit_str
                ])
            profitcol = "Profit"
            if self._fiat_converter:
                profitcol += " (" + fiat_display_currency + ")"

            columns = ['ID', 'Pair', 'Since', profitcol]
            return trades_list, columns, fiat_profit_sum

    def _rpc_daily_profit(
            self, timescale: int,
            stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
        today = datetime.now(timezone.utc).date()
        profit_days: Dict[date, Dict] = {}

        if not (isinstance(timescale, int) and timescale > 0):
            raise RPCException('timescale must be an integer greater than 0')

        for day in range(0, timescale):
            profitday = today - timedelta(days=day)
            trades = Trade.get_trades(trade_filter=[
                Trade.is_open.is_(False),
                Trade.close_date >= profitday,
                Trade.close_date < (profitday + timedelta(days=1))
            ]).order_by(Trade.close_date).all()
            curdayprofit = sum(
                trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
            profit_days[profitday] = {
                'amount': curdayprofit,
                'trades': len(trades)
            }

        data = [
            {
                'date': key,
                'abs_profit': value["amount"],
                'fiat_value': self._fiat_converter.convert_amount(
                    value['amount'],
                    stake_currency,
                    fiat_display_currency
                ) if self._fiat_converter else 0,
                'trade_count': value["trades"],
            }
            for key, value in profit_days.items()
        ]
        return {
            'stake_currency': stake_currency,
            'fiat_display_currency': fiat_display_currency,
            'data': data
        }

    def _rpc_weekly_profit(
            self, timescale: int,
            stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
        today = datetime.now(timezone.utc).date()
        first_iso_day_of_week = today - timedelta(days=today.weekday())  # Monday
        profit_weeks: Dict[date, Dict] = {}

        if not (isinstance(timescale, int) and timescale > 0):
            raise RPCException('timescale must be an integer greater than 0')

        for week in range(0, timescale):
            profitweek = first_iso_day_of_week - timedelta(weeks=week)
            trades = Trade.get_trades(trade_filter=[
                Trade.is_open.is_(False),
                Trade.close_date >= profitweek,
                Trade.close_date < (profitweek + timedelta(weeks=1))
            ]).order_by(Trade.close_date).all()
            curweekprofit = sum(
                trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
            profit_weeks[profitweek] = {
                'amount': curweekprofit,
                'trades': len(trades)
            }

        data = [
            {
                'date': key,
                'abs_profit': value["amount"],
                'fiat_value': self._fiat_converter.convert_amount(
                    value['amount'],
                    stake_currency,
                    fiat_display_currency
                ) if self._fiat_converter else 0,
                'trade_count': value["trades"],
            }
            for key, value in profit_weeks.items()
        ]
        return {
            'stake_currency': stake_currency,
            'fiat_display_currency': fiat_display_currency,
            'data': data
        }

    def _rpc_monthly_profit(
            self, timescale: int,
            stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
        first_day_of_month = datetime.now(timezone.utc).date().replace(day=1)
        profit_months: Dict[date, Dict] = {}

        if not (isinstance(timescale, int) and timescale > 0):
            raise RPCException('timescale must be an integer greater than 0')

        for month in range(0, timescale):
            profitmonth = first_day_of_month - relativedelta(months=month)
            trades = Trade.get_trades(trade_filter=[
                Trade.is_open.is_(False),
                Trade.close_date >= profitmonth,
                Trade.close_date < (profitmonth + relativedelta(months=1))
            ]).order_by(Trade.close_date).all()
            curmonthprofit = sum(
                trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
            profit_months[profitmonth] = {
                'amount': curmonthprofit,
                'trades': len(trades)
            }

        data = [
            {
                'date': f"{key.year}-{key.month:02d}",
                'abs_profit': value["amount"],
                'fiat_value': self._fiat_converter.convert_amount(
                    value['amount'],
                    stake_currency,
                    fiat_display_currency
                ) if self._fiat_converter else 0,
                'trade_count': value["trades"],
            }
            for key, value in profit_months.items()
        ]
        return {
            'stake_currency': stake_currency,
            'fiat_display_currency': fiat_display_currency,
            'data': data
        }

    def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
        """ Returns the X last trades """
        order_by = Trade.id if order_by_id else Trade.close_date.desc()
        if limit:
            trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
                order_by).limit(limit).offset(offset)
        else:
            trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
                Trade.close_date.desc()).all()

        output = [trade.to_json() for trade in trades]

        return {
            "trades": output,
            "trades_count": len(output),
            "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
        }

    def _rpc_stats(self) -> Dict[str, Any]:
        """
        Generate generic stats for trades in database
        """
        def trade_win_loss(trade):
            if trade.close_profit > 0:
                return 'wins'
            elif trade.close_profit < 0:
                return 'losses'
            else:
                return 'draws'
        trades = trades = Trade.get_trades([Trade.is_open.is_(False)])
        # Sell reason
        sell_reasons = {}
        for trade in trades:
            if trade.sell_reason not in sell_reasons:
                sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
            sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1

        # Duration
        dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []}
        for trade in trades:
            if trade.close_date is not None and trade.open_date is not None:
                trade_dur = (trade.close_date - trade.open_date).total_seconds()
                dur[trade_win_loss(trade)].append(trade_dur)

        wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else 'N/A'
        draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else 'N/A'
        losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A'

        durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur}
        return {'sell_reasons': sell_reasons, 'durations': durations}

    def _rpc_trade_statistics(
            self, stake_currency: str, fiat_display_currency: str,
            start_date: datetime = datetime.fromtimestamp(0)) -> Dict[str, Any]:
        """ Returns cumulative profit statistics """
        trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
                        Trade.is_open.is_(True))
        trades = Trade.get_trades(trade_filter).order_by(Trade.id).all()

        profit_all_coin = []
        profit_all_ratio = []
        profit_closed_coin = []
        profit_closed_ratio = []
        durations = []
        winning_trades = 0
        losing_trades = 0

        for trade in trades:
            current_rate: float = 0.0

            if not trade.open_rate:
                continue
            if trade.close_date:
                durations.append((trade.close_date - trade.open_date).total_seconds())

            if not trade.is_open:
                profit_ratio = trade.close_profit
                profit_closed_coin.append(trade.close_profit_abs)
                profit_closed_ratio.append(profit_ratio)
                if trade.close_profit >= 0:
                    winning_trades += 1
                else:
                    losing_trades += 1
            else:
                # Get current rate
                try:
                    current_rate = self._freqtrade.exchange.get_rate(
                        trade.pair, refresh=False, side="sell")
                except (PricingError, ExchangeError):
                    current_rate = NAN
                profit_ratio = trade.calc_profit_ratio(rate=current_rate)

            profit_all_coin.append(
                trade.calc_profit(rate=trade.close_rate or current_rate)
            )
            profit_all_ratio.append(profit_ratio)

        best_pair = Trade.get_best_pair(start_date)

        # Prepare data to display
        profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
        profit_closed_ratio_mean = float(mean(profit_closed_ratio) if profit_closed_ratio else 0.0)
        profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0

        profit_closed_fiat = self._fiat_converter.convert_amount(
            profit_closed_coin_sum,
            stake_currency,
            fiat_display_currency
        ) if self._fiat_converter else 0

        profit_all_coin_sum = round(sum(profit_all_coin), 8)
        profit_all_ratio_mean = float(mean(profit_all_ratio) if profit_all_ratio else 0.0)
        # Doing the sum is not right - overall profit needs to be based on initial capital
        profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
        starting_balance = self._freqtrade.wallets.get_starting_balance()
        profit_closed_ratio_fromstart = 0
        profit_all_ratio_fromstart = 0
        if starting_balance:
            profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
            profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance

        profit_all_fiat = self._fiat_converter.convert_amount(
            profit_all_coin_sum,
            stake_currency,
            fiat_display_currency
        ) if self._fiat_converter else 0

        first_date = trades[0].open_date if trades else None
        last_date = trades[-1].open_date if trades else None
        num = float(len(durations) or 1)
        return {
            'profit_closed_coin': profit_closed_coin_sum,
            'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
            'profit_closed_ratio_mean': profit_closed_ratio_mean,
            'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2),
            'profit_closed_ratio_sum': profit_closed_ratio_sum,
            'profit_closed_ratio': profit_closed_ratio_fromstart,
            'profit_closed_percent': round(profit_closed_ratio_fromstart * 100, 2),
            'profit_closed_fiat': profit_closed_fiat,
            'profit_all_coin': profit_all_coin_sum,
            'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2),
            'profit_all_ratio_mean': profit_all_ratio_mean,
            'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2),
            'profit_all_ratio_sum': profit_all_ratio_sum,
            'profit_all_ratio': profit_all_ratio_fromstart,
            'profit_all_percent': round(profit_all_ratio_fromstart * 100, 2),
            'profit_all_fiat': profit_all_fiat,
            'trade_count': len(trades),
            'closed_trade_count': len([t for t in trades if not t.is_open]),
            'first_trade_date': arrow.get(first_date).humanize() if first_date else '',
            'first_trade_timestamp': int(first_date.timestamp() * 1000) if first_date else 0,
            'latest_trade_date': arrow.get(last_date).humanize() if last_date else '',
            'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0,
            'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
            'best_pair': best_pair[0] if best_pair else '',
            'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0,  # Deprecated
            'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
            'winning_trades': winning_trades,
            'losing_trades': losing_trades,
        }

    def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
        """ Returns current account balance per crypto """
        output = []
        total = 0.0
        try:
            tickers = self._freqtrade.exchange.get_tickers(cached=True)
        except (ExchangeError):
            raise RPCException('Error getting current tickers.')

        self._freqtrade.wallets.update(require_update=False)
        starting_capital = self._freqtrade.wallets.get_starting_balance()
        starting_cap_fiat = self._fiat_converter.convert_amount(
            starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0

        for coin, balance in self._freqtrade.wallets.get_all_balances().items():
            if not balance.total:
                continue

            est_stake: float = 0
            if coin == stake_currency:
                rate = 1.0
                est_stake = balance.total
            else:
                try:
                    pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
                    rate = tickers.get(pair, {}).get('bid', None)
                    if rate:
                        if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
                            rate = 1.0 / rate
                        est_stake = rate * balance.total
                except (ExchangeError):
                    logger.warning(f" Could not get rate for pair {coin}.")
                    continue
            total = total + (est_stake or 0)
            output.append({
                'currency': coin,
                'free': balance.free if balance.free is not None else 0,
                'balance': balance.total if balance.total is not None else 0,
                'used': balance.used if balance.used is not None else 0,
                'est_stake': est_stake or 0,
                'stake': stake_currency,
            })
        if total == 0.0:
            if self._freqtrade.config['dry_run']:
                raise RPCException('Running in Dry Run, balances are not available.')
            else:
                raise RPCException('All balances are zero.')

        value = self._fiat_converter.convert_amount(
            total, stake_currency, fiat_display_currency) if self._fiat_converter else 0

        starting_capital_ratio = 0.0
        starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
        starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0

        return {
            'currencies': output,
            'total': total,
            'symbol': fiat_display_currency,
            'value': value,
            'stake': stake_currency,
            'starting_capital': starting_capital,
            'starting_capital_ratio': starting_capital_ratio,
            'starting_capital_pct': round(starting_capital_ratio * 100, 2),
            'starting_capital_fiat': starting_cap_fiat,
            'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
            'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
            'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
        }

    def _rpc_start(self) -> Dict[str, str]:
        """ Handler for start """
        if self._freqtrade.state == State.RUNNING:
            return {'status': 'already running'}

        self._freqtrade.state = State.RUNNING
        return {'status': 'starting trader ...'}

    def _rpc_stop(self) -> Dict[str, str]:
        """ Handler for stop """
        if self._freqtrade.state == State.RUNNING:
            self._freqtrade.state = State.STOPPED
            return {'status': 'stopping trader ...'}

        return {'status': 'already stopped'}

    def _rpc_reload_config(self) -> Dict[str, str]:
        """ Handler for reload_config. """
        self._freqtrade.state = State.RELOAD_CONFIG
        return {'status': 'Reloading config ...'}

    def _rpc_stopbuy(self) -> Dict[str, str]:
        """
        Handler to stop buying, but handle open trades gracefully.
        """
        if self._freqtrade.state == State.RUNNING:
            # Set 'max_open_trades' to 0
            self._freqtrade.config['max_open_trades'] = 0

        return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}

    def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
        """
        Handler for forcesell <id>.
        Sells the given trade at current price
        """
        def _exec_forcesell(trade: Trade) -> None:
            # Check if there is there is an open order
            fully_canceled = False
            if trade.open_order_id:
                order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)

                if order['side'] == 'buy':
                    fully_canceled = self._freqtrade.handle_cancel_enter(
                        trade, order, CANCEL_REASON['FORCE_SELL'])

                if order['side'] == 'sell':
                    # Cancel order - so it is placed anew with a fresh price.
                    self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])

            if not fully_canceled:
                # Get current rate and execute sell
                current_rate = self._freqtrade.exchange.get_rate(
                    trade.pair, refresh=False, side="sell")
                sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
                order_type = ordertype or self._freqtrade.strategy.order_types.get(
                    "forcesell", self._freqtrade.strategy.order_types["sell"])

                self._freqtrade.execute_trade_exit(
                    trade, current_rate, sell_reason, ordertype=order_type)
        # ---- EOF def _exec_forcesell ----

        if self._freqtrade.state != State.RUNNING:
            raise RPCException('trader is not running')

        with self._freqtrade._exit_lock:
            if trade_id == 'all':
                # Execute sell for all open orders
                for trade in Trade.get_open_trades():
                    _exec_forcesell(trade)
                Trade.commit()
                self._freqtrade.wallets.update()
                return {'result': 'Created sell orders for all open trades.'}

            # Query for trade
            trade = Trade.get_trades(
                trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
            ).first()
            if not trade:
                logger.warning('forcesell: Invalid argument received')
                raise RPCException('invalid argument')

            _exec_forcesell(trade)
            Trade.commit()
            self._freqtrade.wallets.update()
            return {'result': f'Created sell order for trade {trade_id}.'}

    def _rpc_forcebuy(self, pair: str, price: Optional[float],
                      order_type: Optional[str] = None) -> Optional[Trade]:
        """
        Handler for forcebuy <asset> <price>
        Buys a pair trade at the given or current price
        """

        if not self._freqtrade.config.get('forcebuy_enable', False):
            raise RPCException('Forcebuy not enabled.')

        if self._freqtrade.state != State.RUNNING:
            raise RPCException('trader is not running')

        # Check if pair quote currency equals to the stake currency.
        stake_currency = self._freqtrade.config.get('stake_currency')
        if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
            raise RPCException(
                f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.')
        # check if valid pair

        # check if pair already has an open pair
        trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
        if trade:
            raise RPCException(f'position for {pair} already open - id: {trade.id}')

        # gen stake amount
        stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)

        # execute buy
        if not order_type:
            order_type = self._freqtrade.strategy.order_types.get(
                'forcebuy', self._freqtrade.strategy.order_types['buy'])
        if self._freqtrade.execute_entry(pair, stakeamount, price, ordertype=order_type):
            Trade.commit()
            trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
            return trade
        else:
            return None

    def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
        """
        Handler for delete <id>.
        Delete the given trade and close eventually existing open orders.
        """
        with self._freqtrade._exit_lock:
            c_count = 0
            trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
            if not trade:
                logger.warning('delete trade: Invalid argument received')
                raise RPCException('invalid argument')

            # Try cancelling regular order if that exists
            if trade.open_order_id:
                try:
                    self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
                    c_count += 1
                except (ExchangeError):
                    pass

            # cancel stoploss on exchange ...
            if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange')
                    and trade.stoploss_order_id):
                try:
                    self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id,
                                                                   trade.pair)
                    c_count += 1
                except (ExchangeError):
                    pass

            trade.delete()
            self._freqtrade.wallets.update()
            return {
                'result': 'success',
                'trade_id': trade_id,
                'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.',
                'cancel_order_count': c_count,
            }

    def _rpc_performance(self) -> List[Dict[str, Any]]:
        """
        Handler for performance.
        Shows a performance statistic from finished trades
        """
        pair_rates = Trade.get_overall_performance()

        return pair_rates

    def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
        """
        Handler for buy tag performance.
        Shows a performance statistic from finished trades
        """
        buy_tags = Trade.get_buy_tag_performance(pair)

        return buy_tags

    def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
        """
        Handler for sell reason performance.
        Shows a performance statistic from finished trades
        """
        sell_reasons = Trade.get_sell_reason_performance(pair)

        return sell_reasons

    def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
        """
        Handler for mix tag (buy_tag + sell_reason) performance.
        Shows a performance statistic from finished trades
        """
        mix_tags = Trade.get_mix_tag_performance(pair)

        return mix_tags

    def _rpc_count(self) -> Dict[str, float]:
        """ Returns the number of trades running """
        if self._freqtrade.state != State.RUNNING:
            raise RPCException('trader is not running')

        trades = Trade.get_open_trades()
        return {
            'current': len(trades),
            'max': (int(self._freqtrade.config['max_open_trades'])
                    if self._freqtrade.config['max_open_trades'] != float('inf') else -1),
            'total_stake': sum((trade.open_rate * trade.amount) for trade in trades)
        }

    def _rpc_locks(self) -> Dict[str, Any]:
        """ Returns the  current locks """

        locks = PairLocks.get_pair_locks(None)
        return {
            'lock_count': len(locks),
            'locks': [lock.to_json() for lock in locks]
        }

    def _rpc_delete_lock(self, lockid: Optional[int] = None,
                         pair: Optional[str] = None) -> Dict[str, Any]:
        """ Delete specific lock(s) """
        locks = []

        if pair:
            locks = PairLocks.get_pair_locks(pair)
        if lockid:
            locks = PairLock.query.filter(PairLock.id == lockid).all()

        for lock in locks:
            lock.active = False
            lock.lock_end_time = datetime.now(timezone.utc)

        PairLock.query.session.commit()

        return self._rpc_locks()

    def _rpc_whitelist(self) -> Dict:
        """ Returns the currently active whitelist"""
        res = {'method': self._freqtrade.pairlists.name_list,
               'length': len(self._freqtrade.active_pair_whitelist),
               'whitelist': self._freqtrade.active_pair_whitelist
               }
        return res

    def _rpc_blacklist(self, add: List[str] = None) -> Dict:
        """ Returns the currently active blacklist"""
        errors = {}
        if add:
            for pair in add:
                if pair not in self._freqtrade.pairlists.blacklist:
                    try:
                        expand_pairlist([pair], self._freqtrade.exchange.get_markets().keys())
                        self._freqtrade.pairlists.blacklist.append(pair)

                    except ValueError:
                        errors[pair] = {
                            'error_msg': f'Pair {pair} is not a valid wildcard.'}
                else:
                    errors[pair] = {
                        'error_msg': f'Pair {pair} already in pairlist.'}

        res = {'method': self._freqtrade.pairlists.name_list,
               'length': len(self._freqtrade.pairlists.blacklist),
               'blacklist': self._freqtrade.pairlists.blacklist,
               'blacklist_expanded': self._freqtrade.pairlists.expanded_blacklist,
               'errors': errors,
               }
        return res

    @staticmethod
    def _rpc_get_logs(limit: Optional[int]) -> Dict[str, Any]:
        """Returns the last X logs"""
        if limit:
            buffer = bufferHandler.buffer[-limit:]
        else:
            buffer = bufferHandler.buffer
        records = [[datetime.fromtimestamp(r.created).strftime(DATETIME_PRINT_FORMAT),
                   r.created * 1000, r.name, r.levelname,
                   r.message + ('\n' + r.exc_text if r.exc_text else '')]
                   for r in buffer]

        # Log format:
        # [logtime-formatted, logepoch, logger-name, loglevel, message \n + exception]
        # e.g. ["2020-08-27 11:35:01", 1598520901097.9397,
        #       "freqtrade.worker", "INFO", "Starting worker develop"]

        return {'log_count': len(records), 'logs': records}

    def _rpc_edge(self) -> List[Dict[str, Any]]:
        """ Returns information related to Edge """
        if not self._freqtrade.edge:
            raise RPCException('Edge is not enabled.')
        return self._freqtrade.edge.accepted_pairs()

    @staticmethod
    def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame,
                                   last_analyzed: datetime) -> Dict[str, Any]:
        has_content = len(dataframe) != 0
        buy_signals = 0
        sell_signals = 0
        if has_content:

            dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
            # Move signal close to separate column when signal for easy plotting
            if 'buy' in dataframe.columns:
                buy_mask = (dataframe['buy'] == 1)
                buy_signals = int(buy_mask.sum())
                dataframe.loc[buy_mask, '_buy_signal_close'] = dataframe.loc[buy_mask, 'close']
            if 'sell' in dataframe.columns:
                sell_mask = (dataframe['sell'] == 1)
                sell_signals = int(sell_mask.sum())
                dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close']
            dataframe = dataframe.replace([inf, -inf], NAN)
            dataframe = dataframe.replace({NAN: None})

        res = {
            'pair': pair,
            'timeframe': timeframe,
            'timeframe_ms': timeframe_to_msecs(timeframe),
            'strategy': strategy,
            'columns': list(dataframe.columns),
            'data': dataframe.values.tolist(),
            'length': len(dataframe),
            'buy_signals': buy_signals,
            'sell_signals': sell_signals,
            'last_analyzed': last_analyzed,
            'last_analyzed_ts': int(last_analyzed.timestamp()),
            'data_start': '',
            'data_start_ts': 0,
            'data_stop': '',
            'data_stop_ts': 0,
        }
        if has_content:
            res.update({
                'data_start': str(dataframe.iloc[0]['date']),
                'data_start_ts': int(dataframe.iloc[0]['__date_ts']),
                'data_stop': str(dataframe.iloc[-1]['date']),
                'data_stop_ts': int(dataframe.iloc[-1]['__date_ts']),
            })
        return res

    def _rpc_analysed_dataframe(self, pair: str, timeframe: str,
                                limit: Optional[int]) -> Dict[str, Any]:

        _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(
            pair, timeframe)
        _data = _data.copy()
        if limit:
            _data = _data.iloc[-limit:]
        return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'],
                                               pair, timeframe, _data, last_analyzed)

    @staticmethod
    def _rpc_analysed_history_full(config, pair: str, timeframe: str,
                                   timerange: str) -> Dict[str, Any]:
        timerange_parsed = TimeRange.parse_timerange(timerange)

        _data = load_data(
            datadir=config.get("datadir"),
            pairs=[pair],
            timeframe=timeframe,
            timerange=timerange_parsed,
            data_format=config.get('dataformat_ohlcv', 'json'),
        )
        if pair not in _data:
            raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.")
        from freqtrade.data.dataprovider import DataProvider
        from freqtrade.resolvers.strategy_resolver import StrategyResolver
        strategy = StrategyResolver.load_strategy(config)
        strategy.dp = DataProvider(config, exchange=None, pairlists=None)

        df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})

        return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe,
                                              df_analyzed, arrow.Arrow.utcnow().datetime)

    def _rpc_plot_config(self) -> Dict[str, Any]:
        if (self._freqtrade.strategy.plot_config and
                'subplots' not in self._freqtrade.strategy.plot_config):
            self._freqtrade.strategy.plot_config['subplots'] = {}
        return self._freqtrade.strategy.plot_config

    @staticmethod
    def _rpc_sysinfo() -> Dict[str, Any]:
        return {
            "cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
            "ram_pct": psutil.virtual_memory().percent
        }
Beispiel #24
0
def test_fiat_convert_two_FIAT(mocker):
    patch_coinmarketcap(mocker)
    fiat_convert = CryptoToFiatConverter()

    assert fiat_convert.get_price(crypto_symbol='USD',
                                  fiat_symbol='EUR') == 0.0
Beispiel #25
0
def test_rpc_balance_handle(default_conf, mocker):
    mock_balance = {
        'BTC': {
            'free': 10.0,
            'total': 12.0,
            'used': 2.0,
        },
        'ETH': {
            'free': 1.0,
            'total': 5.0,
            'used': 4.0,
        },
        'PAX': {
            'free': 5.0,
            'total': 10.0,
            'used': 5.0,
        }
    }

    mocker.patch.multiple(
        'freqtrade.rpc.fiat_convert.Market',
        ticker=MagicMock(return_value={'price_usd': 15000.0}),
    )
    patch_exchange(mocker)
    mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    mocker.patch.multiple(
        'freqtrade.exchange.Exchange',
        get_balances=MagicMock(return_value=mock_balance),
        get_ticker=MagicMock(
            side_effect=lambda p, r: {'bid': 100} if p == "BTC/PAX" else {'bid': 0.01}),
        get_valid_pair_combination=MagicMock(
            side_effect=lambda a, b: f"{b}/{a}" if a == "PAX" else f"{a}/{b}")
    )

    freqtradebot = FreqtradeBot(default_conf)
    patch_get_signal(freqtradebot, (True, False))
    rpc = RPC(freqtradebot)
    rpc._fiat_converter = CryptoToFiatConverter()

    result = rpc._rpc_balance(default_conf['fiat_display_currency'])
    assert prec_satoshi(result['total'], 12.15)
    assert prec_satoshi(result['value'], 182250)
    assert 'USD' == result['symbol']
    assert result['currencies'] == [
        {'currency': 'BTC',
            'free': 10.0,
            'balance': 12.0,
            'used': 2.0,
            'est_btc': 12.0,
         },
        {'free': 1.0,
         'balance': 5.0,
         'currency': 'ETH',
         'est_btc': 0.05,
         'used': 4.0
         },
        {'free': 5.0,
         'balance': 10.0,
         'currency': 'PAX',
         'est_btc': 0.1,
         'used': 5.0}
    ]
    assert result['total'] == 12.15
Beispiel #26
0
def test_fiat_convert_same_currencies(mocker):
    patch_coinmarketcap(mocker)
    fiat_convert = CryptoToFiatConverter()

    assert fiat_convert.get_price(crypto_symbol='USD',
                                  fiat_symbol='USD') == 1.0
Beispiel #27
0
def test_loadcryptomap(mocker):

    fiat_convert = CryptoToFiatConverter()
    assert len(fiat_convert._cryptomap) == 2

    assert fiat_convert._cryptomap["btc"] == "bitcoin"
Beispiel #28
0
def test_fiat_convert_unsupported_crypto(mocker, caplog):
    mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
    fiat_convert = CryptoToFiatConverter()
    assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
    assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog)
Beispiel #29
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('forcebuy', self._forcebuy),
            CommandHandler('performance', self._performance),
            CommandHandler('daily', self._daily),
            CommandHandler('count', self._count),
            CommandHandler('reload_conf', self._reload_conf),
            CommandHandler('whitelist', self._whitelist),
            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"
                       "*Sell Reason:* `{sell_reason}`\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_tab = 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_tab}</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']:
                if currency['est_btc'] > 0.0001:
                    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)
                else:
                    output += "*{currency}:* not showing <1$ amount \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 _forcebuy(self, bot: Bot, update: Update) -> None:
        """
        Handler for /forcebuy <asset> <price>.
        Buys a pair trade at the given or current price
        :param bot: telegram bot
        :param update: message update
        :return: None
        """

        message = update.message.text.replace('/forcebuy', '').strip().split()
        pair = message[0]
        price = float(message[1]) if len(message) > 1 else None
        try:
            self._rpc_forcebuy(pair, price)
        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 _whitelist(self, bot: Bot, update: Update) -> None:
        """
        Handler for /whitelist
        Shows the currently active whitelist
        """
        try:
            whitelist = self._rpc_whitelist()

            message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n"
            message += f"`{', '.join(whitelist['whitelist'])}`"

            logger.debug(message)
            self._send_msg(message)
        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" \
                  "*/reload_conf:* `Reload configuration file` \n" \
                  "*/whitelist:* `Show current whitelist` \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 #30
0
def test_fiat_convert_is_supported(mocker):
    fiat_convert = CryptoToFiatConverter()
    assert fiat_convert._is_supported_fiat(fiat='USD') is True
    assert fiat_convert._is_supported_fiat(fiat='usd') is True
    assert fiat_convert._is_supported_fiat(fiat='abc') is False
    assert fiat_convert._is_supported_fiat(fiat='ABC') is False