Merge pull request #3818 from freqtrade/rpc/candlehistory
Rpc/candlehistory
This commit is contained in:
commit
301598bac9
@ -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]
|
||||
```
|
||||
|
||||
## Available commands
|
||||
## Available endpoints
|
||||
|
||||
| Command | Description |
|
||||
|----------|-------------|
|
||||
@ -129,8 +129,17 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
||||
| `whitelist` | Show the current whitelist
|
||||
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||
| `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
|
||||
|
||||
!!! 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.
|
||||
|
||||
``` bash
|
||||
@ -140,6 +149,12 @@ python3 scripts/rest_client.py help
|
||||
``` output
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
Return the performance of the different coins.
|
||||
|
||||
plot_config
|
||||
Return plot configuration if the strategy defines one.
|
||||
|
||||
profit
|
||||
Return the profit summary.
|
||||
|
||||
@ -204,6 +237,14 @@ stop
|
||||
stopbuy
|
||||
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
|
||||
Return trades history.
|
||||
|
||||
@ -215,7 +256,6 @@ version
|
||||
whitelist
|
||||
Show the current whitelist.
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Advanced API usage using JWT tokens
|
||||
|
@ -51,7 +51,8 @@ class IResolver:
|
||||
:param object_name: Class name of the object
|
||||
:param enum_failed: If True, will return None for modules which fail.
|
||||
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
|
||||
@ -67,14 +68,16 @@ class IResolver:
|
||||
return iter([None])
|
||||
|
||||
valid_objects_gen = (
|
||||
obj for name, obj in inspect.getmembers(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)
|
||||
(obj, inspect.getsource(module)) for
|
||||
name, obj in inspect.getmembers(
|
||||
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
|
||||
|
||||
@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]]:
|
||||
"""
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
@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]:
|
||||
"""
|
||||
Try to load object from path list.
|
||||
@ -106,7 +112,8 @@ class IResolver:
|
||||
for _path in paths:
|
||||
try:
|
||||
(module, module_path) = cls._search_object(directory=_path,
|
||||
object_name=object_name)
|
||||
object_name=object_name,
|
||||
add_source=add_source)
|
||||
if module:
|
||||
logger.info(
|
||||
f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} "
|
||||
@ -118,7 +125,7 @@ class IResolver:
|
||||
return None
|
||||
|
||||
@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:
|
||||
"""
|
||||
Search and loads the specified object as configured in hte child class.
|
||||
@ -133,10 +140,10 @@ class IResolver:
|
||||
user_subdir=cls.user_subdir,
|
||||
extra_dir=extra_dir)
|
||||
|
||||
pairlist = cls._load_object(paths=abs_paths, object_name=object_name,
|
||||
kwargs=kwargs)
|
||||
if pairlist:
|
||||
return pairlist
|
||||
found_object = cls._load_object(paths=abs_paths, object_name=object_name,
|
||||
kwargs=kwargs)
|
||||
if found_object:
|
||||
return found_object
|
||||
raise OperationalException(
|
||||
f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist "
|
||||
"or contains Python code errors."
|
||||
@ -164,8 +171,8 @@ class IResolver:
|
||||
for obj in cls._get_valid_object(module_path, object_name=None,
|
||||
enum_failed=enum_failed):
|
||||
objects.append(
|
||||
{'name': obj.__name__ if obj is not None else '',
|
||||
'class': obj,
|
||||
{'name': obj[0].__name__ if obj is not None else '',
|
||||
'class': obj[0] if obj is not None else None,
|
||||
'location': entry,
|
||||
})
|
||||
return objects
|
||||
|
@ -174,7 +174,9 @@ class StrategyResolver(IResolver):
|
||||
|
||||
strategy = StrategyResolver._load_object(paths=abs_paths,
|
||||
object_name=strategy_name,
|
||||
kwargs={'config': config})
|
||||
add_source=True,
|
||||
kwargs={'config': config},
|
||||
)
|
||||
if strategy:
|
||||
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
|
||||
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
|
||||
|
@ -1,7 +1,9 @@
|
||||
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
|
||||
@ -15,7 +17,8 @@ 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
|
||||
@ -26,15 +29,15 @@ 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
|
||||
@ -212,6 +211,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'])
|
||||
@ -227,7 +240,7 @@ class ApiServer(RPC):
|
||||
"""
|
||||
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
|
||||
@ -247,7 +260,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
|
||||
|
||||
@ -262,7 +275,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
|
||||
@ -272,7 +285,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
|
||||
@ -282,7 +295,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
|
||||
@ -292,14 +305,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
|
||||
@ -307,7 +320,7 @@ class ApiServer(RPC):
|
||||
"""
|
||||
Prints the bot's version
|
||||
"""
|
||||
return self.rest_dump({"version": __version__})
|
||||
return jsonify({"version": __version__})
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
@ -315,7 +328,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
|
||||
@ -325,7 +338,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
|
||||
@ -335,7 +348,7 @@ 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
|
||||
@ -353,7 +366,7 @@ class ApiServer(RPC):
|
||||
self._config.get('fiat_display_currency', '')
|
||||
)
|
||||
|
||||
return self.rest_dump(stats)
|
||||
return jsonify(stats)
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
@ -365,7 +378,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
|
||||
@ -376,7 +389,7 @@ class ApiServer(RPC):
|
||||
"""
|
||||
stats = self._rpc_edge()
|
||||
|
||||
return self.rest_dump(stats)
|
||||
return jsonify(stats)
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
@ -392,7 +405,7 @@ class ApiServer(RPC):
|
||||
self._config.get('fiat_display_currency')
|
||||
)
|
||||
|
||||
return self.rest_dump(stats)
|
||||
return jsonify(stats)
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
@ -405,7 +418,7 @@ class ApiServer(RPC):
|
||||
"""
|
||||
stats = self._rpc_performance()
|
||||
|
||||
return self.rest_dump(stats)
|
||||
return jsonify(stats)
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
@ -417,9 +430,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
|
||||
@ -431,7 +444,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
|
||||
@ -443,7 +456,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
|
||||
@ -456,7 +469,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
|
||||
@ -465,7 +478,7 @@ class ApiServer(RPC):
|
||||
Handler for /whitelist.
|
||||
"""
|
||||
results = self._rpc_whitelist()
|
||||
return self.rest_dump(results)
|
||||
return jsonify(results)
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
@ -475,7 +488,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
|
||||
@ -487,9 +500,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
|
||||
@ -499,4 +512,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 = 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)
|
||||
|
@ -9,9 +9,12 @@ 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.configuration.timerange import TimeRange
|
||||
from freqtrade.constants import CANCEL_REASON
|
||||
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
|
||||
@ -90,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'],
|
||||
@ -117,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
|
||||
|
||||
@ -653,3 +655,80 @@ class RPC:
|
||||
if not self._freqtrade.edge:
|
||||
raise RPCException('Edge is not enabled.')
|
||||
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
|
||||
|
@ -755,7 +755,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"
|
||||
|
@ -123,6 +123,8 @@ class IStrategy(ABC):
|
||||
# and wallets - access to the current balance.
|
||||
dp: Optional[DataProvider] = None
|
||||
wallets: Optional[Wallets] = None
|
||||
# container variable for strategy source code
|
||||
__source__: str = ''
|
||||
|
||||
# Definition of plot_config. See plotting documentation for more details.
|
||||
plot_config: Dict = {}
|
||||
|
@ -224,6 +224,70 @@ class FtRestClient():
|
||||
|
||||
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():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
@ -146,6 +146,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||
:return: FreqtradeBot
|
||||
"""
|
||||
patch_freqtradebot(mocker, config)
|
||||
config['datadir'] = Path(config['datadir'])
|
||||
return FreqtradeBot(config)
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ Unit test file for rpc/api_server.py
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
@ -811,3 +812,176 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
|
||||
data='{"tradeid": "1"}')
|
||||
assert_response(rc)
|
||||
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
|
||||
|
@ -18,13 +18,15 @@ def test_search_strategy():
|
||||
|
||||
s, _ = StrategyResolver._search_object(
|
||||
directory=default_location,
|
||||
object_name='DefaultStrategy'
|
||||
object_name='DefaultStrategy',
|
||||
add_source=True,
|
||||
)
|
||||
assert issubclass(s, IStrategy)
|
||||
|
||||
s, _ = StrategyResolver._search_object(
|
||||
directory=default_location,
|
||||
object_name='NotFoundStrategy'
|
||||
object_name='NotFoundStrategy',
|
||||
add_source=True,
|
||||
)
|
||||
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 = 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'})
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user