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]
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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 = {}
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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'})
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user