Merge branch 'develop' into pr/imxuwang/3799

This commit is contained in:
Matthias
2020-10-22 07:55:48 +02:00
174 changed files with 4148 additions and 1648 deletions

View File

@@ -1,2 +1,3 @@
from .rpc import RPC, RPCMessageType, RPCException # noqa
from .rpc_manager import RPCManager # noqa
# flake8: noqa: F401
from .rpc import RPC, RPCException, RPCMessageType
from .rpc_manager import RPCManager

View File

@@ -1,40 +1,43 @@
import logging
import threading
from copy import deepcopy
from datetime import date, datetime
from ipaddress import IPv4Address
from pathlib import Path
from typing import Any, Callable, Dict
from arrow import Arrow
from flask import Flask, jsonify, request
from flask.json import JSONEncoder
from flask_cors import CORS
from flask_jwt_extended import (JWTManager, create_access_token,
create_refresh_token, get_jwt_identity,
jwt_refresh_token_required,
from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token,
get_jwt_identity, jwt_refresh_token_required,
verify_jwt_in_request_optional)
from werkzeug.security import safe_str_cmp
from werkzeug.serving import make_server
from freqtrade.__init__ import __version__
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import Trade
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.rpc.rpc import RPC, RPCException
logger = logging.getLogger(__name__)
BASE_URI = "/api/v1"
class ArrowJSONEncoder(JSONEncoder):
class FTJSONEncoder(JSONEncoder):
def default(self, obj):
try:
if isinstance(obj, Arrow):
return obj.for_json()
elif isinstance(obj, date):
return obj.strftime("%Y-%m-%d")
elif isinstance(obj, datetime):
return obj.strftime(DATETIME_PRINT_FORMAT)
elif isinstance(obj, date):
return obj.strftime("%Y-%m-%d")
iterable = iter(obj)
except TypeError:
pass
@@ -108,7 +111,7 @@ class ApiServer(RPC):
'jwt_secret_key', 'super-secret')
self.jwt = JWTManager(self.app)
self.app.json_encoder = ArrowJSONEncoder
self.app.json_encoder = FTJSONEncoder
self.app.teardown_appcontext(shutdown_session)
@@ -160,16 +163,12 @@ class ApiServer(RPC):
"""
pass
def rest_dump(self, return_value):
""" Helper function to jsonify object for a webserver """
return jsonify(return_value)
def rest_error(self, error_msg):
return jsonify({"error": error_msg}), 502
def rest_error(self, error_msg, error_code=502):
return jsonify({"error": error_msg}), error_code
def register_rest_rpc_urls(self):
"""
Registers flask app URLs that are calls to functonality in rpc.rpc.
Registers flask app URLs that are calls to functionality in rpc.rpc.
First two arguments passed are /URL and 'Label'
Label can be used as a shortcut when refactoring
@@ -193,6 +192,7 @@ class ApiServer(RPC):
self.app.add_url_rule(f'{BASE_URI}/balance', 'balance',
view_func=self._balance, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/locks', 'locks', view_func=self._locks, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET'])
@@ -212,6 +212,20 @@ class ApiServer(RPC):
view_func=self._trades, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/trades/<int:tradeid>', 'trades_delete',
view_func=self._trades_delete, methods=['DELETE'])
self.app.add_url_rule(f'{BASE_URI}/pair_candles', 'pair_candles',
view_func=self._analysed_candles, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/pair_history', 'pair_history',
view_func=self._analysed_history, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/plot_config', 'plot_config',
view_func=self._plot_config, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/strategies', 'strategies',
view_func=self._list_strategies, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/strategy/<string:strategy>', 'strategy',
view_func=self._get_strategy, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/available_pairs', 'pairs',
view_func=self._list_available_pairs, methods=['GET'])
# Combined actions and infos
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
methods=['GET', 'POST'])
@@ -222,15 +236,12 @@ class ApiServer(RPC):
self.app.add_url_rule(f'{BASE_URI}/forcesell', 'forcesell', view_func=self._forcesell,
methods=['POST'])
# TODO: Implement the following
# help (?)
@require_login
def page_not_found(self, error):
"""
Return "404 not found", 404.
"""
return self.rest_dump({
return jsonify({
'status': 'error',
'reason': f"There's no API call for {request.base_url}.",
'code': 404
@@ -250,7 +261,7 @@ class ApiServer(RPC):
'access_token': create_access_token(identity=keystuff),
'refresh_token': create_refresh_token(identity=keystuff),
}
return self.rest_dump(ret)
return jsonify(ret)
return jsonify({"error": "Unauthorized"}), 401
@@ -265,7 +276,7 @@ class ApiServer(RPC):
new_token = create_access_token(identity=current_user, fresh=False)
ret = {'access_token': new_token}
return self.rest_dump(ret)
return jsonify(ret)
@require_login
@rpc_catch_errors
@@ -275,7 +286,7 @@ class ApiServer(RPC):
Starts TradeThread in bot if stopped.
"""
msg = self._rpc_start()
return self.rest_dump(msg)
return jsonify(msg)
@require_login
@rpc_catch_errors
@@ -285,7 +296,7 @@ class ApiServer(RPC):
Stops TradeThread in bot if running
"""
msg = self._rpc_stop()
return self.rest_dump(msg)
return jsonify(msg)
@require_login
@rpc_catch_errors
@@ -295,14 +306,14 @@ class ApiServer(RPC):
Sets max_open_trades to 0 and gracefully sells all open trades
"""
msg = self._rpc_stopbuy()
return self.rest_dump(msg)
return jsonify(msg)
@rpc_catch_errors
def _ping(self):
"""
simple poing version
simple ping version
"""
return self.rest_dump({"status": "pong"})
return jsonify({"status": "pong"})
@require_login
@rpc_catch_errors
@@ -310,7 +321,7 @@ class ApiServer(RPC):
"""
Prints the bot's version
"""
return self.rest_dump({"version": __version__})
return jsonify({"version": __version__})
@require_login
@rpc_catch_errors
@@ -318,7 +329,7 @@ class ApiServer(RPC):
"""
Prints the bot's version
"""
return self.rest_dump(self._rpc_show_config())
return jsonify(self._rpc_show_config(self._config))
@require_login
@rpc_catch_errors
@@ -328,7 +339,7 @@ class ApiServer(RPC):
Triggers a config file reload
"""
msg = self._rpc_reload_config()
return self.rest_dump(msg)
return jsonify(msg)
@require_login
@rpc_catch_errors
@@ -338,7 +349,16 @@ class ApiServer(RPC):
Returns the number of trades running
"""
msg = self._rpc_count()
return self.rest_dump(msg)
return jsonify(msg)
@require_login
@rpc_catch_errors
def _locks(self):
"""
Handler for /locks.
Returns the currently active locks.
"""
return jsonify(self._rpc_locks())
@require_login
@rpc_catch_errors
@@ -356,7 +376,7 @@ class ApiServer(RPC):
self._config.get('fiat_display_currency', '')
)
return self.rest_dump(stats)
return jsonify(stats)
@require_login
@rpc_catch_errors
@@ -368,7 +388,7 @@ class ApiServer(RPC):
limit: Only get a certain number of records
"""
limit = int(request.args.get('limit', 0)) or None
return self.rest_dump(self._rpc_get_logs(limit))
return jsonify(self._rpc_get_logs(limit))
@require_login
@rpc_catch_errors
@@ -379,7 +399,7 @@ class ApiServer(RPC):
"""
stats = self._rpc_edge()
return self.rest_dump(stats)
return jsonify(stats)
@require_login
@rpc_catch_errors
@@ -395,7 +415,7 @@ class ApiServer(RPC):
self._config.get('fiat_display_currency')
)
return self.rest_dump(stats)
return jsonify(stats)
@require_login
@rpc_catch_errors
@@ -408,7 +428,7 @@ class ApiServer(RPC):
"""
stats = self._rpc_performance()
return self.rest_dump(stats)
return jsonify(stats)
@require_login
@rpc_catch_errors
@@ -420,9 +440,9 @@ class ApiServer(RPC):
"""
try:
results = self._rpc_trade_status()
return self.rest_dump(results)
return jsonify(results)
except RPCException:
return self.rest_dump([])
return jsonify([])
@require_login
@rpc_catch_errors
@@ -434,7 +454,7 @@ class ApiServer(RPC):
"""
results = self._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', ''))
return self.rest_dump(results)
return jsonify(results)
@require_login
@rpc_catch_errors
@@ -446,7 +466,7 @@ class ApiServer(RPC):
"""
limit = int(request.args.get('limit', 0))
results = self._rpc_trade_history(limit)
return self.rest_dump(results)
return jsonify(results)
@require_login
@rpc_catch_errors
@@ -459,7 +479,7 @@ class ApiServer(RPC):
tradeid: Numeric trade-id assigned to the trade.
"""
result = self._rpc_delete(tradeid)
return self.rest_dump(result)
return jsonify(result)
@require_login
@rpc_catch_errors
@@ -468,7 +488,7 @@ class ApiServer(RPC):
Handler for /whitelist.
"""
results = self._rpc_whitelist()
return self.rest_dump(results)
return jsonify(results)
@require_login
@rpc_catch_errors
@@ -478,7 +498,7 @@ class ApiServer(RPC):
"""
add = request.json.get("blacklist", None) if request.method == 'POST' else None
results = self._rpc_blacklist(add)
return self.rest_dump(results)
return jsonify(results)
@require_login
@rpc_catch_errors
@@ -490,9 +510,9 @@ class ApiServer(RPC):
price = request.json.get("price", None)
trade = self._rpc_forcebuy(asset, price)
if trade:
return self.rest_dump(trade.to_json())
return jsonify(trade.to_json())
else:
return self.rest_dump({"status": f"Error buying pair {asset}."})
return jsonify({"status": f"Error buying pair {asset}."})
@require_login
@rpc_catch_errors
@@ -502,4 +522,132 @@ class ApiServer(RPC):
"""
tradeid = request.json.get("tradeid")
results = self._rpc_forcesell(tradeid)
return self.rest_dump(results)
return jsonify(results)
@require_login
@rpc_catch_errors
def _analysed_candles(self):
"""
Handler for /pair_candles.
Returns the dataframe the bot is using during live/dry operations.
Takes the following get arguments:
get:
parameters:
- pair: Pair
- timeframe: Timeframe to get data for (should be aligned to strategy.timeframe)
- limit: Limit return length to the latest X candles
"""
pair = request.args.get("pair")
timeframe = request.args.get("timeframe")
limit = request.args.get("limit", type=int)
if not pair or not timeframe:
return self.rest_error("Mandatory parameter missing.", 400)
results = self._rpc_analysed_dataframe(pair, timeframe, limit)
return jsonify(results)
@require_login
@rpc_catch_errors
def _analysed_history(self):
"""
Handler for /pair_history.
Returns the dataframe of a given timerange
Takes the following get arguments:
get:
parameters:
- pair: Pair
- timeframe: Timeframe to get data for (should be aligned to strategy.timeframe)
- strategy: Strategy to use - Must exist in configured strategy-path!
- timerange: timerange in the format YYYYMMDD-YYYYMMDD (YYYYMMDD- or (-YYYYMMDD))
are als possible. If omitted uses all available data.
"""
pair = request.args.get("pair")
timeframe = request.args.get("timeframe")
timerange = request.args.get("timerange")
strategy = request.args.get("strategy")
if not pair or not timeframe or not timerange or not strategy:
return self.rest_error("Mandatory parameter missing.", 400)
config = deepcopy(self._config)
config.update({
'strategy': strategy,
})
results = RPC._rpc_analysed_history_full(config, pair, timeframe, timerange)
return jsonify(results)
@require_login
@rpc_catch_errors
def _plot_config(self):
"""
Handler for /plot_config.
"""
return jsonify(self._rpc_plot_config())
@require_login
@rpc_catch_errors
def _list_strategies(self):
directory = Path(self._config.get(
'strategy_path', self._config['user_data_dir'] / USERPATH_STRATEGIES))
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy_objs = StrategyResolver.search_all_objects(directory, False)
strategy_objs = sorted(strategy_objs, key=lambda x: x['name'])
return jsonify({'strategies': [x['name'] for x in strategy_objs]})
@require_login
@rpc_catch_errors
def _get_strategy(self, strategy: str):
"""
Get a single strategy
get:
parameters:
- strategy: Only get this strategy
"""
config = deepcopy(self._config)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
try:
strategy_obj = StrategyResolver._load_strategy(strategy, config,
extra_dir=config.get('strategy_path'))
except OperationalException:
return self.rest_error("Strategy not found.", 404)
return jsonify({
'strategy': strategy_obj.get_strategy_name(),
'code': strategy_obj.__source__,
})
@require_login
@rpc_catch_errors
def _list_available_pairs(self):
"""
Handler for /available_pairs.
Returns an object, with pairs, available pair length and pair_interval combinations
Takes the following get arguments:
get:
parameters:
- stake_currency: Filter on this stake currency
- timeframe: Timeframe to get data for Filter elements to this timeframe
"""
timeframe = request.args.get("timeframe")
stake_currency = request.args.get("stake_currency")
from freqtrade.data.history import get_datahandler
dh = get_datahandler(self._config['datadir'], self._config.get('dataformat_ohlcv', None))
pair_interval = dh.ohlcv_get_available_data(self._config['datadir'])
if timeframe:
pair_interval = [pair for pair in pair_interval if pair[1] == timeframe]
if stake_currency:
pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)]
pair_interval = sorted(pair_interval, key=lambda x: x[0])
pairs = list({x[0] for x in pair_interval})
result = {
'length': len(pairs),
'pairs': pairs,
'pair_interval': pair_interval,
}
return jsonify(result)

View File

@@ -9,25 +9,29 @@ from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union
import arrow
from numpy import NAN, mean
from numpy import NAN, int64, mean
from pandas import DataFrame
from freqtrade.constants import CANCEL_REASON
from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
from freqtrade.data.history import load_data
from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler
from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade
from freqtrade.persistence import PairLock, Trade
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State
from freqtrade.strategy.interface import SellType
logger = logging.getLogger(__name__)
class RPCMessageType(Enum):
STATUS_NOTIFICATION = 'status'
WARNING_NOTIFICATION = 'warning'
CUSTOM_NOTIFICATION = 'custom'
STARTUP_NOTIFICATION = 'startup'
BUY_NOTIFICATION = 'buy'
BUY_CANCEL_NOTIFICATION = 'buy_cancel'
SELL_NOTIFICATION = 'sell'
@@ -36,6 +40,9 @@ class RPCMessageType(Enum):
def __repr__(self):
return self.value
def __str__(self):
return self.value
class RPCException(Exception):
"""
@@ -86,13 +93,12 @@ class RPC:
def send_msg(self, msg: Dict[str, str]) -> None:
""" Sends a message to all registered rpc modules """
def _rpc_show_config(self) -> Dict[str, Any]:
def _rpc_show_config(self, config) -> Dict[str, Any]:
"""
Return a dict of config options.
Explicitly does NOT return the full config to avoid leakage of sensitive
information via rpc.
"""
config = self._freqtrade.config
val = {
'dry_run': config['dry_run'],
'stake_currency': config['stake_currency'],
@@ -113,7 +119,7 @@ class RPC:
'forcebuy_enabled': config.get('forcebuy_enable', False),
'ask_strategy': config.get('ask_strategy', {}),
'bid_strategy': config.get('bid_strategy', {}),
'state': str(self._freqtrade.state)
'state': str(self._freqtrade.state) if self._freqtrade else '',
}
return val
@@ -562,8 +568,7 @@ class RPC:
except (ExchangeError):
pass
Trade.session.delete(trade)
Trade.session.flush()
trade.delete()
self._freqtrade.wallets.update()
return {
'result': 'success',
@@ -594,6 +599,17 @@ class RPC:
'total_stake': sum((trade.open_rate * trade.amount) for trade in trades)
}
def _rpc_locks(self) -> Dict[str, Any]:
""" Returns the current locks"""
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
locks = PairLock.get_pair_locks(None)
return {
'lock_count': len(locks),
'locks': [lock.to_json() for lock in locks]
}
def _rpc_whitelist(self) -> Dict:
""" Returns the currently active whitelist"""
res = {'method': self._freqtrade.pairlists.name_list,
@@ -633,7 +649,7 @@ class RPC:
buffer = bufferHandler.buffer[-limit:]
else:
buffer = bufferHandler.buffer
records = [[datetime.fromtimestamp(r.created).strftime("%Y-%m-%d %H:%M:%S"),
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]
@@ -650,3 +666,82 @@ class RPC:
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'].astype(int64) // 1000 // 1000
# Move open to seperate 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_open'] = dataframe.loc[buy_mask, 'open']
if 'sell' in dataframe.columns:
sell_mask = (dataframe['sell'] == 1)
sell_signals = int(sell_mask.sum())
dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open']
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: 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'),
)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config)
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]:
return self._freqtrade.strategy.plot_config

View File

@@ -6,6 +6,7 @@ from typing import Any, Dict, List
from freqtrade.rpc import RPC, RPCMessageType
logger = logging.getLogger(__name__)
@@ -59,7 +60,7 @@ class RPCManager:
try:
mod.send_msg(msg)
except NotImplementedError:
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.")
logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.")
def startup_messages(self, config: Dict[str, Any], pairlist) -> None:
if config['dry_run']:
@@ -76,7 +77,7 @@ class RPCManager:
exchange_name = config['exchange']['name']
strategy_name = config.get('strategy', '')
self.send_msg({
'type': RPCMessageType.CUSTOM_NOTIFICATION,
'type': RPCMessageType.STARTUP_NOTIFICATION,
'status': f'*Exchange:* `{exchange_name}`\n'
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
f'*Minimum ROI:* `{minimal_roi}`\n'
@@ -85,7 +86,7 @@ class RPCManager:
f'*Strategy:* `{strategy_name}`'
})
self.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'type': RPCMessageType.STARTUP_NOTIFICATION,
'status': f'Searching for {stake_currency} pairs to buy and sell '
f'based on {pairlist.short_desc()}'
})

View File

@@ -5,9 +5,9 @@ This module manage Telegram communication
"""
import json
import logging
import arrow
from typing import Any, Callable, Dict, List
import arrow
from tabulate import tabulate
from telegram import ParseMode, ReplyKeyboardMarkup, Update
from telegram.error import NetworkError, TelegramError
@@ -18,6 +18,7 @@ from freqtrade.__init__ import __version__
from freqtrade.rpc import RPC, RPCException, RPCMessageType
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...')
@@ -99,6 +100,7 @@ class Telegram(RPC):
CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
CommandHandler('locks', self._locks),
CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
CommandHandler(['show_config', 'show_conf'], self._show_config),
CommandHandler('stopbuy', self._stopbuy),
@@ -133,6 +135,13 @@ class Telegram(RPC):
def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """
noti = self._config['telegram'].get('notification_settings', {}
).get(str(msg['type']), 'on')
if noti == 'off':
logger.info(f"Notification '{msg['type']}' not sent.")
# Notification disabled
return
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
if self._fiat_converter:
msg['stake_amount_fiat'] = self._fiat_converter.convert_amount(
@@ -191,13 +200,13 @@ class Telegram(RPC):
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION:
message = '{status}'.format(**msg)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
self._send_msg(message)
self._send_msg(message, disable_notification=(noti == 'silent'))
def _get_sell_emoji(self, msg):
"""
@@ -601,6 +610,26 @@ class Telegram(RPC):
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _locks(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /locks.
Returns the currently active locks
"""
try:
locks = self._rpc_locks()
message = tabulate([[
lock['pair'],
lock['lock_end_time'],
lock['reason']] for lock in locks['locks']],
headers=['Pair', 'Until', 'Reason'],
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:
"""
@@ -712,8 +741,8 @@ class Telegram(RPC):
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\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"
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
"*/locks:* `Show currently locked pairs`\n"
"*/balance:* `Show account balance per currency`\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/reload_config:* `Reload configuration file` \n"
@@ -804,7 +833,7 @@ class Telegram(RPC):
:param update: message update
:return: None
"""
val = self._rpc_show_config()
val = self._rpc_show_config(self._freqtrade.config)
if val['trailing_stop']:
sl_info = (
f"*Initial Stoploss:* `{val['stoploss']}`\n"
@@ -830,7 +859,8 @@ class Telegram(RPC):
f"*Current state:* `{val['state']}`"
)
def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN,
disable_notification: bool = False) -> None:
"""
Send given markdown message
:param msg: message
@@ -851,7 +881,8 @@ class Telegram(RPC):
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
reply_markup=reply_markup
reply_markup=reply_markup,
disable_notification=disable_notification,
)
except NetworkError as network_err:
# Sometimes the telegram server resets the current connection,
@@ -864,7 +895,8 @@ class Telegram(RPC):
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
reply_markup=reply_markup
reply_markup=reply_markup,
disable_notification=disable_notification,
)
except TelegramError as telegram_err:
logger.warning(

View File

@@ -2,9 +2,9 @@
This module manages webhook communication
"""
import logging
from typing import Any, Dict
from typing import Any, Dict
from requests import post, RequestException
from requests import RequestException, post
from freqtrade.rpc import RPC, RPCMessageType
@@ -48,13 +48,13 @@ class Webhook(RPC):
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
valuedict = self._config['webhook'].get('webhooksellcancel', None)
elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.CUSTOM_NOTIFICATION,
RPCMessageType.STARTUP_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION):
valuedict = self._config['webhook'].get('webhookstatus', None)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
if not valuedict:
logger.info("Message type %s not configured for webhooks", msg['type'])
logger.info("Message type '%s' not configured for webhooks", msg['type'])
return
payload = {key: value.format(**msg) for (key, value) in valuedict.items()}