Merge pull request #3818 from freqtrade/rpc/candlehistory

Rpc/candlehistory
This commit is contained in:
Matthias 2020-10-04 09:34:25 +02:00 committed by GitHub
commit 301598bac9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 579 additions and 64 deletions

View File

@ -104,7 +104,7 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters] python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters]
``` ```
## Available commands ## Available endpoints
| Command | Description | | Command | Description |
|----------|-------------| |----------|-------------|
@ -129,8 +129,17 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `whitelist` | Show the current whitelist | `whitelist` | Show the current whitelist
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
| `edge` | Show validated pairs by Edge if it is enabled. | `edge` | Show validated pairs by Edge if it is enabled.
| `pair_candles` | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
| `pair_history` | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
| `plot_config` | Get plot config from the strategy (or nothing if not configured). **Alpha**
| `strategies` | List strategies in strategy directory. **Alpha**
| `strategy <strategy>` | Get specific Strategy content. **Alpha**
| `available_pairs` | List available backtest data. **Alpha**
| `version` | Show version | `version` | Show version
!!! Warning "Alpha status"
Endpoints labeled with *Alpha status* above may change at any time without notice.
Possible commands can be listed from the rest-client script using the `help` command. Possible commands can be listed from the rest-client script using the `help` command.
``` bash ``` bash
@ -140,6 +149,12 @@ python3 scripts/rest_client.py help
``` output ``` output
Possible commands: Possible commands:
available_pairs
Return available pair (backtest data) based on timeframe / stake_currency selection
:param timeframe: Only pairs with this timeframe available.
:param stake_currency: Only pairs that include this timeframe
balance balance
Get the account balance. Get the account balance.
@ -179,9 +194,27 @@ logs
:param limit: Limits log messages to the last <limit> logs. No limit to get all the trades. :param limit: Limits log messages to the last <limit> logs. No limit to get all the trades.
pair_candles
Return live dataframe for <pair><timeframe>.
:param pair: Pair to get data for
:param timeframe: Only pairs with this timeframe available.
:param limit: Limit result to the last n candles.
pair_history
Return historic, analyzed dataframe
:param pair: Pair to get data for
:param timeframe: Only pairs with this timeframe available.
:param strategy: Strategy to analyze and get values for
:param timerange: Timerange to get data for (same format than --timerange endpoints)
performance performance
Return the performance of the different coins. Return the performance of the different coins.
plot_config
Return plot configuration if the strategy defines one.
profit profit
Return the profit summary. Return the profit summary.
@ -204,6 +237,14 @@ stop
stopbuy stopbuy
Stop buying (but handle sells gracefully). Use `reload_config` to reset. Stop buying (but handle sells gracefully). Use `reload_config` to reset.
strategies
Lists available strategies
strategy
Get strategy details
:param strategy: Strategy class name
trades trades
Return trades history. Return trades history.
@ -215,7 +256,6 @@ version
whitelist whitelist
Show the current whitelist. Show the current whitelist.
``` ```
## Advanced API usage using JWT tokens ## Advanced API usage using JWT tokens

View File

@ -51,7 +51,8 @@ class IResolver:
:param object_name: Class name of the object :param object_name: Class name of the object
:param enum_failed: If True, will return None for modules which fail. :param enum_failed: If True, will return None for modules which fail.
Otherwise, failing modules are skipped. Otherwise, failing modules are skipped.
:return: generator containing matching objects :return: generator containing tuple of matching objects
Tuple format: [Object, source]
""" """
# Generate spec based on absolute path # Generate spec based on absolute path
@ -67,14 +68,16 @@ class IResolver:
return iter([None]) return iter([None])
valid_objects_gen = ( valid_objects_gen = (
obj for name, obj in inspect.getmembers(module, inspect.isclass) (obj, inspect.getsource(module)) for
if ((object_name is None or object_name == name) and name, obj in inspect.getmembers(
issubclass(obj, cls.object_type) and obj is not cls.object_type) module, inspect.isclass) if ((object_name is None or object_name == name)
and issubclass(obj, cls.object_type)
and obj is not cls.object_type)
) )
return valid_objects_gen return valid_objects_gen
@classmethod @classmethod
def _search_object(cls, directory: Path, object_name: str def _search_object(cls, directory: Path, *, object_name: str, add_source: bool = False
) -> Union[Tuple[Any, Path], Tuple[None, None]]: ) -> Union[Tuple[Any, Path], Tuple[None, None]]:
""" """
Search for the objectname in the given directory Search for the objectname in the given directory
@ -93,11 +96,14 @@ class IResolver:
obj = next(cls._get_valid_object(module_path, object_name), None) obj = next(cls._get_valid_object(module_path, object_name), None)
if obj: if obj:
return (obj, module_path) obj[0].__file__ = str(entry)
if add_source:
obj[0].__source__ = obj[1]
return (obj[0], module_path)
return (None, None) return (None, None)
@classmethod @classmethod
def _load_object(cls, paths: List[Path], object_name: str, def _load_object(cls, paths: List[Path], *, object_name: str, add_source: bool = False,
kwargs: dict = {}) -> Optional[Any]: kwargs: dict = {}) -> Optional[Any]:
""" """
Try to load object from path list. Try to load object from path list.
@ -106,7 +112,8 @@ class IResolver:
for _path in paths: for _path in paths:
try: try:
(module, module_path) = cls._search_object(directory=_path, (module, module_path) = cls._search_object(directory=_path,
object_name=object_name) object_name=object_name,
add_source=add_source)
if module: if module:
logger.info( logger.info(
f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} " f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} "
@ -118,7 +125,7 @@ class IResolver:
return None return None
@classmethod @classmethod
def load_object(cls, object_name: str, config: dict, kwargs: dict, def load_object(cls, object_name: str, config: dict, *, kwargs: dict,
extra_dir: Optional[str] = None) -> Any: extra_dir: Optional[str] = None) -> Any:
""" """
Search and loads the specified object as configured in hte child class. Search and loads the specified object as configured in hte child class.
@ -133,10 +140,10 @@ class IResolver:
user_subdir=cls.user_subdir, user_subdir=cls.user_subdir,
extra_dir=extra_dir) extra_dir=extra_dir)
pairlist = cls._load_object(paths=abs_paths, object_name=object_name, found_object = cls._load_object(paths=abs_paths, object_name=object_name,
kwargs=kwargs) kwargs=kwargs)
if pairlist: if found_object:
return pairlist return found_object
raise OperationalException( raise OperationalException(
f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist " f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist "
"or contains Python code errors." "or contains Python code errors."
@ -164,8 +171,8 @@ class IResolver:
for obj in cls._get_valid_object(module_path, object_name=None, for obj in cls._get_valid_object(module_path, object_name=None,
enum_failed=enum_failed): enum_failed=enum_failed):
objects.append( objects.append(
{'name': obj.__name__ if obj is not None else '', {'name': obj[0].__name__ if obj is not None else '',
'class': obj, 'class': obj[0] if obj is not None else None,
'location': entry, 'location': entry,
}) })
return objects return objects

View File

@ -174,7 +174,9 @@ class StrategyResolver(IResolver):
strategy = StrategyResolver._load_object(paths=abs_paths, strategy = StrategyResolver._load_object(paths=abs_paths,
object_name=strategy_name, object_name=strategy_name,
kwargs={'config': config}) add_source=True,
kwargs={'config': config},
)
if strategy: if strategy:
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)

View File

@ -1,7 +1,9 @@
import logging import logging
import threading import threading
from copy import deepcopy
from datetime import date, datetime from datetime import date, datetime
from ipaddress import IPv4Address from ipaddress import IPv4Address
from pathlib import Path
from typing import Any, Callable, Dict from typing import Any, Callable, Dict
from arrow import Arrow from arrow import Arrow
@ -15,7 +17,8 @@ from werkzeug.security import safe_str_cmp
from werkzeug.serving import make_server from werkzeug.serving import make_server
from freqtrade.__init__ import __version__ 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.persistence import Trade
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
@ -26,15 +29,15 @@ logger = logging.getLogger(__name__)
BASE_URI = "/api/v1" BASE_URI = "/api/v1"
class ArrowJSONEncoder(JSONEncoder): class FTJSONEncoder(JSONEncoder):
def default(self, obj): def default(self, obj):
try: try:
if isinstance(obj, Arrow): if isinstance(obj, Arrow):
return obj.for_json() return obj.for_json()
elif isinstance(obj, date):
return obj.strftime("%Y-%m-%d")
elif isinstance(obj, datetime): elif isinstance(obj, datetime):
return obj.strftime(DATETIME_PRINT_FORMAT) return obj.strftime(DATETIME_PRINT_FORMAT)
elif isinstance(obj, date):
return obj.strftime("%Y-%m-%d")
iterable = iter(obj) iterable = iter(obj)
except TypeError: except TypeError:
pass pass
@ -108,7 +111,7 @@ class ApiServer(RPC):
'jwt_secret_key', 'super-secret') 'jwt_secret_key', 'super-secret')
self.jwt = JWTManager(self.app) self.jwt = JWTManager(self.app)
self.app.json_encoder = ArrowJSONEncoder self.app.json_encoder = FTJSONEncoder
self.app.teardown_appcontext(shutdown_session) self.app.teardown_appcontext(shutdown_session)
@ -160,16 +163,12 @@ class ApiServer(RPC):
""" """
pass pass
def rest_dump(self, return_value): def rest_error(self, error_msg, error_code=502):
""" Helper function to jsonify object for a webserver """ return jsonify({"error": error_msg}), error_code
return jsonify(return_value)
def rest_error(self, error_msg):
return jsonify({"error": error_msg}), 502
def register_rest_rpc_urls(self): 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' First two arguments passed are /URL and 'Label'
Label can be used as a shortcut when refactoring Label can be used as a shortcut when refactoring
@ -212,6 +211,20 @@ class ApiServer(RPC):
view_func=self._trades, methods=['GET']) view_func=self._trades, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/trades/<int:tradeid>', 'trades_delete', self.app.add_url_rule(f'{BASE_URI}/trades/<int:tradeid>', 'trades_delete',
view_func=self._trades_delete, methods=['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 # Combined actions and infos
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
methods=['GET', 'POST']) methods=['GET', 'POST'])
@ -227,7 +240,7 @@ class ApiServer(RPC):
""" """
Return "404 not found", 404. Return "404 not found", 404.
""" """
return self.rest_dump({ return jsonify({
'status': 'error', 'status': 'error',
'reason': f"There's no API call for {request.base_url}.", 'reason': f"There's no API call for {request.base_url}.",
'code': 404 'code': 404
@ -247,7 +260,7 @@ class ApiServer(RPC):
'access_token': create_access_token(identity=keystuff), 'access_token': create_access_token(identity=keystuff),
'refresh_token': create_refresh_token(identity=keystuff), 'refresh_token': create_refresh_token(identity=keystuff),
} }
return self.rest_dump(ret) return jsonify(ret)
return jsonify({"error": "Unauthorized"}), 401 return jsonify({"error": "Unauthorized"}), 401
@ -262,7 +275,7 @@ class ApiServer(RPC):
new_token = create_access_token(identity=current_user, fresh=False) new_token = create_access_token(identity=current_user, fresh=False)
ret = {'access_token': new_token} ret = {'access_token': new_token}
return self.rest_dump(ret) return jsonify(ret)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -272,7 +285,7 @@ class ApiServer(RPC):
Starts TradeThread in bot if stopped. Starts TradeThread in bot if stopped.
""" """
msg = self._rpc_start() msg = self._rpc_start()
return self.rest_dump(msg) return jsonify(msg)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -282,7 +295,7 @@ class ApiServer(RPC):
Stops TradeThread in bot if running Stops TradeThread in bot if running
""" """
msg = self._rpc_stop() msg = self._rpc_stop()
return self.rest_dump(msg) return jsonify(msg)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -292,14 +305,14 @@ class ApiServer(RPC):
Sets max_open_trades to 0 and gracefully sells all open trades Sets max_open_trades to 0 and gracefully sells all open trades
""" """
msg = self._rpc_stopbuy() msg = self._rpc_stopbuy()
return self.rest_dump(msg) return jsonify(msg)
@rpc_catch_errors @rpc_catch_errors
def _ping(self): def _ping(self):
""" """
simple poing version simple ping version
""" """
return self.rest_dump({"status": "pong"}) return jsonify({"status": "pong"})
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -307,7 +320,7 @@ class ApiServer(RPC):
""" """
Prints the bot's version Prints the bot's version
""" """
return self.rest_dump({"version": __version__}) return jsonify({"version": __version__})
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -315,7 +328,7 @@ class ApiServer(RPC):
""" """
Prints the bot's version Prints the bot's version
""" """
return self.rest_dump(self._rpc_show_config()) return jsonify(self._rpc_show_config(self._config))
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -325,7 +338,7 @@ class ApiServer(RPC):
Triggers a config file reload Triggers a config file reload
""" """
msg = self._rpc_reload_config() msg = self._rpc_reload_config()
return self.rest_dump(msg) return jsonify(msg)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -335,7 +348,7 @@ class ApiServer(RPC):
Returns the number of trades running Returns the number of trades running
""" """
msg = self._rpc_count() msg = self._rpc_count()
return self.rest_dump(msg) return jsonify(msg)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -353,7 +366,7 @@ class ApiServer(RPC):
self._config.get('fiat_display_currency', '') self._config.get('fiat_display_currency', '')
) )
return self.rest_dump(stats) return jsonify(stats)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -365,7 +378,7 @@ class ApiServer(RPC):
limit: Only get a certain number of records limit: Only get a certain number of records
""" """
limit = int(request.args.get('limit', 0)) or None 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 @require_login
@rpc_catch_errors @rpc_catch_errors
@ -376,7 +389,7 @@ class ApiServer(RPC):
""" """
stats = self._rpc_edge() stats = self._rpc_edge()
return self.rest_dump(stats) return jsonify(stats)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -392,7 +405,7 @@ class ApiServer(RPC):
self._config.get('fiat_display_currency') self._config.get('fiat_display_currency')
) )
return self.rest_dump(stats) return jsonify(stats)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -405,7 +418,7 @@ class ApiServer(RPC):
""" """
stats = self._rpc_performance() stats = self._rpc_performance()
return self.rest_dump(stats) return jsonify(stats)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -417,9 +430,9 @@ class ApiServer(RPC):
""" """
try: try:
results = self._rpc_trade_status() results = self._rpc_trade_status()
return self.rest_dump(results) return jsonify(results)
except RPCException: except RPCException:
return self.rest_dump([]) return jsonify([])
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -431,7 +444,7 @@ class ApiServer(RPC):
""" """
results = self._rpc_balance(self._config['stake_currency'], results = self._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', '')) self._config.get('fiat_display_currency', ''))
return self.rest_dump(results) return jsonify(results)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -443,7 +456,7 @@ class ApiServer(RPC):
""" """
limit = int(request.args.get('limit', 0)) limit = int(request.args.get('limit', 0))
results = self._rpc_trade_history(limit) results = self._rpc_trade_history(limit)
return self.rest_dump(results) return jsonify(results)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -456,7 +469,7 @@ class ApiServer(RPC):
tradeid: Numeric trade-id assigned to the trade. tradeid: Numeric trade-id assigned to the trade.
""" """
result = self._rpc_delete(tradeid) result = self._rpc_delete(tradeid)
return self.rest_dump(result) return jsonify(result)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -465,7 +478,7 @@ class ApiServer(RPC):
Handler for /whitelist. Handler for /whitelist.
""" """
results = self._rpc_whitelist() results = self._rpc_whitelist()
return self.rest_dump(results) return jsonify(results)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -475,7 +488,7 @@ class ApiServer(RPC):
""" """
add = request.json.get("blacklist", None) if request.method == 'POST' else None add = request.json.get("blacklist", None) if request.method == 'POST' else None
results = self._rpc_blacklist(add) results = self._rpc_blacklist(add)
return self.rest_dump(results) return jsonify(results)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -487,9 +500,9 @@ class ApiServer(RPC):
price = request.json.get("price", None) price = request.json.get("price", None)
trade = self._rpc_forcebuy(asset, price) trade = self._rpc_forcebuy(asset, price)
if trade: if trade:
return self.rest_dump(trade.to_json()) return jsonify(trade.to_json())
else: else:
return self.rest_dump({"status": f"Error buying pair {asset}."}) return jsonify({"status": f"Error buying pair {asset}."})
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -499,4 +512,132 @@ class ApiServer(RPC):
""" """
tradeid = request.json.get("tradeid") tradeid = request.json.get("tradeid")
results = self._rpc_forcesell(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 = self._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,9 +9,12 @@ from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
import arrow import arrow
from numpy import NAN, mean from numpy import NAN, int64, mean
from pandas import DataFrame
from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON from freqtrade.constants import CANCEL_REASON
from freqtrade.data.history import load_data
from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler from freqtrade.loggers import bufferHandler
@ -90,13 +93,12 @@ class RPC:
def send_msg(self, msg: Dict[str, str]) -> None: def send_msg(self, msg: Dict[str, str]) -> None:
""" Sends a message to all registered rpc modules """ """ 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. Return a dict of config options.
Explicitly does NOT return the full config to avoid leakage of sensitive Explicitly does NOT return the full config to avoid leakage of sensitive
information via rpc. information via rpc.
""" """
config = self._freqtrade.config
val = { val = {
'dry_run': config['dry_run'], 'dry_run': config['dry_run'],
'stake_currency': config['stake_currency'], 'stake_currency': config['stake_currency'],
@ -117,7 +119,7 @@ class RPC:
'forcebuy_enabled': config.get('forcebuy_enable', False), 'forcebuy_enabled': config.get('forcebuy_enable', False),
'ask_strategy': config.get('ask_strategy', {}), 'ask_strategy': config.get('ask_strategy', {}),
'bid_strategy': config.get('bid_strategy', {}), 'bid_strategy': config.get('bid_strategy', {}),
'state': str(self._freqtrade.state) 'state': str(self._freqtrade.state) if self._freqtrade else '',
} }
return val return val
@ -653,3 +655,80 @@ class RPC:
if not self._freqtrade.edge: if not self._freqtrade.edge:
raise RPCException('Edge is not enabled.') raise RPCException('Edge is not enabled.')
return self._freqtrade.edge.accepted_pairs() return self._freqtrade.edge.accepted_pairs()
def _convert_dataframe_to_dict(self, 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)
def _rpc_analysed_history_full(self, 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 self._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

@ -755,7 +755,7 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
val = self._rpc_show_config() val = self._rpc_show_config(self._freqtrade.config)
if val['trailing_stop']: if val['trailing_stop']:
sl_info = ( sl_info = (
f"*Initial Stoploss:* `{val['stoploss']}`\n" f"*Initial Stoploss:* `{val['stoploss']}`\n"

View File

@ -123,6 +123,8 @@ class IStrategy(ABC):
# and wallets - access to the current balance. # and wallets - access to the current balance.
dp: Optional[DataProvider] = None dp: Optional[DataProvider] = None
wallets: Optional[Wallets] = None wallets: Optional[Wallets] = None
# container variable for strategy source code
__source__: str = ''
# Definition of plot_config. See plotting documentation for more details. # Definition of plot_config. See plotting documentation for more details.
plot_config: Dict = {} plot_config: Dict = {}

View File

@ -224,6 +224,70 @@ class FtRestClient():
return self._post("forcesell", data={"tradeid": tradeid}) return self._post("forcesell", data={"tradeid": tradeid})
def strategies(self):
"""Lists available strategies
:return: json object
"""
return self._get("strategies")
def strategy(self, strategy):
"""Get strategy details
:param strategy: Strategy class name
:return: json object
"""
return self._get(f"strategy/{strategy}")
def plot_config(self):
"""Return plot configuration if the strategy defines one.
:return: json object
"""
return self._get("plot_config")
def available_pairs(self, timeframe=None, stake_currency=None):
"""Return available pair (backtest data) based on timeframe / stake_currency selection
:param timeframe: Only pairs with this timeframe available.
:param stake_currency: Only pairs that include this timeframe
:return: json object
"""
return self._get("available_pairs", params={
"stake_currency": stake_currency if timeframe else '',
"timeframe": timeframe if timeframe else '',
})
def pair_candles(self, pair, timeframe, limit=None):
"""Return live dataframe for <pair><timeframe>.
:param pair: Pair to get data for
:param timeframe: Only pairs with this timeframe available.
:param limit: Limit result to the last n candles.
:return: json object
"""
return self._get("available_pairs", params={
"pair": pair,
"timeframe": timeframe,
"limit": limit,
})
def pair_history(self, pair, timeframe, strategy, timerange=None):
"""Return historic, analyzed dataframe
:param pair: Pair to get data for
:param timeframe: Only pairs with this timeframe available.
:param strategy: Strategy to analyze and get values for
:param timerange: Timerange to get data for (same format than --timerange endpoints)
:return: json object
"""
return self._get("pair_history", params={
"pair": pair,
"timeframe": timeframe,
"strategy": strategy,
"timerange": timerange if timerange else '',
})
def add_arguments(): def add_arguments():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()

View File

@ -146,6 +146,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
:return: FreqtradeBot :return: FreqtradeBot
""" """
patch_freqtradebot(mocker, config) patch_freqtradebot(mocker, config)
config['datadir'] = Path(config['datadir'])
return FreqtradeBot(config) return FreqtradeBot(config)

View File

@ -3,6 +3,7 @@ Unit test file for rpc/api_server.py
""" """
from datetime import datetime from datetime import datetime
from pathlib import Path
from unittest.mock import ANY, MagicMock, PropertyMock from unittest.mock import ANY, MagicMock, PropertyMock
import pytest import pytest
@ -811,3 +812,176 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
data='{"tradeid": "1"}') data='{"tradeid": "1"}')
assert_response(rc) assert_response(rc)
assert rc.json == {'result': 'Created sell order for trade 1.'} assert rc.json == {'result': 'Created sell order for trade 1.'}
def test_api_pair_candles(botclient, ohlcv_history):
ftbot, client = botclient
timeframe = '5m'
amount = 2
# No pair
rc = client_get(client,
f"{BASE_URI}/pair_candles?limit={amount}&timeframe={timeframe}")
assert_response(rc, 400)
# No timeframe
rc = client_get(client,
f"{BASE_URI}/pair_candles?pair=XRP%2FBTC")
assert_response(rc, 400)
rc = client_get(client,
f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}")
assert_response(rc)
assert 'columns' in rc.json
assert 'data_start_ts' in rc.json
assert 'data_start' in rc.json
assert 'data_stop' in rc.json
assert 'data_stop_ts' in rc.json
assert len(rc.json['data']) == 0
ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean()
ohlcv_history['buy'] = 0
ohlcv_history.loc[1, 'buy'] = 1
ohlcv_history['sell'] = 0
ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history)
rc = client_get(client,
f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}")
assert_response(rc)
assert 'strategy' in rc.json
assert rc.json['strategy'] == 'DefaultStrategy'
assert 'columns' in rc.json
assert 'data_start_ts' in rc.json
assert 'data_start' in rc.json
assert 'data_stop' in rc.json
assert 'data_stop_ts' in rc.json
assert rc.json['data_start'] == '2017-11-26 08:50:00+00:00'
assert rc.json['data_start_ts'] == 1511686200000
assert rc.json['data_stop'] == '2017-11-26 08:55:00+00:00'
assert rc.json['data_stop_ts'] == 1511686500000
assert isinstance(rc.json['columns'], list)
assert rc.json['columns'] == ['date', 'open', 'high',
'low', 'close', 'volume', 'sma', 'buy', 'sell',
'__date_ts', '_buy_signal_open', '_sell_signal_open']
assert 'pair' in rc.json
assert rc.json['pair'] == 'XRP/BTC'
assert 'data' in rc.json
assert len(rc.json['data']) == amount
assert (rc.json['data'] ==
[['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869,
None, 0, 0, 1511686200000, None, None],
['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05,
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None]
])
def test_api_pair_history(botclient, ohlcv_history):
ftbot, client = botclient
timeframe = '5m'
# No pair
rc = client_get(client,
f"{BASE_URI}/pair_history?timeframe={timeframe}"
"&timerange=20180111-20180112&strategy=DefaultStrategy")
assert_response(rc, 400)
# No Timeframe
rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC"
"&timerange=20180111-20180112&strategy=DefaultStrategy")
assert_response(rc, 400)
# No timerange
rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&strategy=DefaultStrategy")
assert_response(rc, 400)
# No strategy
rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&timerange=20180111-20180112")
assert_response(rc, 400)
# Working
rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&timerange=20180111-20180112&strategy=DefaultStrategy")
assert_response(rc, 200)
assert rc.json['length'] == 289
assert len(rc.json['data']) == rc.json['length']
assert 'columns' in rc.json
assert 'data' in rc.json
assert rc.json['pair'] == 'UNITTEST/BTC'
assert rc.json['strategy'] == 'DefaultStrategy'
assert rc.json['data_start'] == '2018-01-11 00:00:00+00:00'
assert rc.json['data_start_ts'] == 1515628800000
assert rc.json['data_stop'] == '2018-01-12 00:00:00+00:00'
assert rc.json['data_stop_ts'] == 1515715200000
def test_api_plot_config(botclient):
ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/plot_config")
assert_response(rc)
assert rc.json == {}
ftbot.strategy.plot_config = {'main_plot': {'sma': {}},
'subplots': {'RSI': {'rsi': {'color': 'red'}}}}
rc = client_get(client, f"{BASE_URI}/plot_config")
assert_response(rc)
assert rc.json == ftbot.strategy.plot_config
assert isinstance(rc.json['main_plot'], dict)
def test_api_strategies(botclient):
ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/strategies")
assert_response(rc)
assert rc.json == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']}
def test_api_strategy(botclient):
ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/strategy/DefaultStrategy")
assert_response(rc)
assert rc.json['strategy'] == 'DefaultStrategy'
data = (Path(__file__).parents[1] / "strategy/strats/default_strategy.py").read_text()
assert rc.json['code'] == data
rc = client_get(client, f"{BASE_URI}/strategy/NoStrat")
assert_response(rc, 404)
def test_list_available_pairs(botclient):
ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/available_pairs")
assert_response(rc)
assert rc.json['length'] == 12
assert isinstance(rc.json['pairs'], list)
rc = client_get(client, f"{BASE_URI}/available_pairs?timeframe=5m")
assert_response(rc)
assert rc.json['length'] == 12
rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH")
assert_response(rc)
assert rc.json['length'] == 1
assert rc.json['pairs'] == ['XRP/ETH']
assert len(rc.json['pair_interval']) == 2
rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH&timeframe=5m")
assert_response(rc)
assert rc.json['length'] == 1
assert rc.json['pairs'] == ['XRP/ETH']
assert len(rc.json['pair_interval']) == 1

View File

@ -18,13 +18,15 @@ def test_search_strategy():
s, _ = StrategyResolver._search_object( s, _ = StrategyResolver._search_object(
directory=default_location, directory=default_location,
object_name='DefaultStrategy' object_name='DefaultStrategy',
add_source=True,
) )
assert issubclass(s, IStrategy) assert issubclass(s, IStrategy)
s, _ = StrategyResolver._search_object( s, _ = StrategyResolver._search_object(
directory=default_location, directory=default_location,
object_name='NotFoundStrategy' object_name='NotFoundStrategy',
add_source=True,
) )
assert s is None assert s is None
@ -53,6 +55,9 @@ def test_load_strategy(default_conf, result):
'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates')
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert isinstance(strategy.__source__, str)
assert 'class SampleStrategy' in strategy.__source__
assert isinstance(strategy.__file__, str)
assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'})