Merge pull request #10015 from freqtrade/feat/clients
Add freqtrade-clients as individually installable dependency
This commit is contained in:
@@ -60,11 +60,16 @@ jobs:
|
|||||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
|
pip install -e ft_client/
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
|
- name: Check for version alignment
|
||||||
|
run: |
|
||||||
|
python build_helpers/freqtrade_client_version_align.py
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
|
||||||
|
|
||||||
- name: Coveralls
|
- name: Coveralls
|
||||||
if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04')
|
||||||
@@ -188,6 +193,7 @@ jobs:
|
|||||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
|
pip install -e ft_client/
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
@@ -398,13 +404,14 @@ jobs:
|
|||||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
|
pip install -e ft_client/
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
- name: Tests incl. ccxt compatibility tests
|
- name: Tests incl. ccxt compatibility tests
|
||||||
env:
|
env:
|
||||||
CI_WEB_PROXY: http://152.67.78.211:13128
|
CI_WEB_PROXY: http://152.67.78.211:13128
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --longrun --durations 20 -n auto --dist loadscope
|
pytest --random-order --longrun --durations 20 -n auto
|
||||||
|
|
||||||
|
|
||||||
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
||||||
@@ -467,6 +474,19 @@ jobs:
|
|||||||
dist
|
dist
|
||||||
retention-days: 10
|
retention-days: 10
|
||||||
|
|
||||||
|
- name: Build Client distribution
|
||||||
|
run: |
|
||||||
|
pip install -U build
|
||||||
|
python -m build --sdist --wheel ft_client
|
||||||
|
|
||||||
|
- name: Upload artifacts 📦
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: freqtrade-client-build
|
||||||
|
path: |
|
||||||
|
ft_client/dist
|
||||||
|
retention-days: 10
|
||||||
|
|
||||||
deploy-pypi:
|
deploy-pypi:
|
||||||
name: "Deploy to PyPI"
|
name: "Deploy to PyPI"
|
||||||
needs: [ build ]
|
needs: [ build ]
|
||||||
@@ -484,8 +504,10 @@ jobs:
|
|||||||
- name: Download artifact 📦
|
- name: Download artifact 📦
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: freqtrade-build
|
name: freqtrade*-build
|
||||||
path: dist
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
- name: Publish to PyPI (Test)
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.14
|
uses: pypa/gh-action-pypi-publish@v1.8.14
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from freqtrade_client import __version__ as client_version
|
||||||
|
|
||||||
|
from freqtrade import __version__ as ft_version
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if ft_version != client_version:
|
||||||
|
print(f"Versions do not match: \n"
|
||||||
|
f"ft: {ft_version} \n"
|
||||||
|
f"client: {client_version}")
|
||||||
|
exit(1)
|
||||||
|
print(f"Versions match: ft: {ft_version}, client: {client_version}")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
+7
-5
@@ -95,11 +95,13 @@ Make sure that the following 2 lines are available in your docker-compose file:
|
|||||||
|
|
||||||
### Consuming the API
|
### Consuming the API
|
||||||
|
|
||||||
You can consume the API by using the script `scripts/rest_client.py`.
|
You can consume the API by using `freqtrade-client` (also available as `scripts/rest_client.py`).
|
||||||
The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system.
|
This command can be installed independent of the bot by using `pip install freqtrade-client`.
|
||||||
|
|
||||||
|
This module is designed to be lightweight, and only depends on the `requests` and `python-rapidjson` modules, skipping all heavy dependencies freqtrade otherwise needs.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
python3 scripts/rest_client.py <command> [optional parameters]
|
freqtrade-client <command> [optional parameters]
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour.
|
By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour.
|
||||||
@@ -120,7 +122,7 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
|
|||||||
```
|
```
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters]
|
freqtrade-client --config rest_config.json <command> [optional parameters]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available endpoints
|
### Available endpoints
|
||||||
@@ -176,7 +178,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
|||||||
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
|
||||||
python3 scripts/rest_client.py help
|
freqtrade-client help
|
||||||
```
|
```
|
||||||
|
|
||||||
``` output
|
``` output
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../LICENSE
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
include LICENSE
|
||||||
|
include README.md
|
||||||
|
|
||||||
|
prune tests
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Freqtrade Client
|
||||||
|
|
||||||
|
# 
|
||||||
|
|
||||||
|
Provides a minimal rest client for the freqtrade rest api.
|
||||||
|
|
||||||
|
Please check out the [main project](https://github.com/freqtrade/freqtrade) for more information or details.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from freqtrade_client.ft_rest_client import FtRestClient
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = '2024.3-dev'
|
||||||
|
|
||||||
|
if 'dev' in __version__:
|
||||||
|
from pathlib import Path
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
freqtrade_basedir = Path(__file__).parent
|
||||||
|
|
||||||
|
__version__ = __version__ + '-' + subprocess.check_output(
|
||||||
|
['git', 'log', '--format="%h"', '-n 1'],
|
||||||
|
stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).decode("utf-8").rstrip().strip('"')
|
||||||
|
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
# git not available, ignore
|
||||||
|
try:
|
||||||
|
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
||||||
|
versionfile = Path('./freqtrade_commit')
|
||||||
|
if versionfile.is_file():
|
||||||
|
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
__all__ = ['FtRestClient']
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import argparse
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import rapidjson
|
||||||
|
from freqtrade_client import __version__
|
||||||
|
from freqtrade_client.ft_rest_client import FtRestClient
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("ft_rest_client")
|
||||||
|
|
||||||
|
|
||||||
|
def add_arguments(args: Any = None):
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("command",
|
||||||
|
help="Positional argument defining the command to execute.",
|
||||||
|
nargs="?"
|
||||||
|
)
|
||||||
|
parser.add_argument('-V', '--version', action='version', version=f'%(prog)s {__version__}')
|
||||||
|
parser.add_argument('--show',
|
||||||
|
help='Show possible methods with this client',
|
||||||
|
dest='show',
|
||||||
|
action='store_true',
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('-c', '--config',
|
||||||
|
help='Specify configuration file (default: %(default)s). ',
|
||||||
|
dest='config',
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
|
default='config.json'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("command_arguments",
|
||||||
|
help="Positional arguments for the parameters for [command]",
|
||||||
|
nargs="*",
|
||||||
|
default=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
pargs = parser.parse_args(args)
|
||||||
|
return vars(pargs)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(configfile):
|
||||||
|
file = Path(configfile)
|
||||||
|
if file.is_file():
|
||||||
|
with file.open("r") as f:
|
||||||
|
config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS |
|
||||||
|
rapidjson.PM_TRAILING_COMMAS)
|
||||||
|
return config
|
||||||
|
else:
|
||||||
|
logger.warning(f"Could not load config file {file}.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def print_commands():
|
||||||
|
# Print dynamic help for the different commands using the commands doc-strings
|
||||||
|
client = FtRestClient(None)
|
||||||
|
print("Possible commands:\n")
|
||||||
|
for x, y in inspect.getmembers(client):
|
||||||
|
if not x.startswith('_'):
|
||||||
|
doc = re.sub(':return:.*', '', getattr(client, x).__doc__, flags=re.MULTILINE).rstrip()
|
||||||
|
print(f"{x}\n\t{doc}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main_exec(args: Dict[str, Any]):
|
||||||
|
|
||||||
|
if args.get("show"):
|
||||||
|
print_commands()
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
config = load_config(args['config'])
|
||||||
|
url = config.get('api_server', {}).get('listen_ip_address', '127.0.0.1')
|
||||||
|
port = config.get('api_server', {}).get('listen_port', '8080')
|
||||||
|
username = config.get('api_server', {}).get('username')
|
||||||
|
password = config.get('api_server', {}).get('password')
|
||||||
|
|
||||||
|
server_url = f"http://{url}:{port}"
|
||||||
|
client = FtRestClient(server_url, username, password)
|
||||||
|
|
||||||
|
m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')]
|
||||||
|
command = args["command"]
|
||||||
|
if command not in m:
|
||||||
|
logger.error(f"Command {command} not defined")
|
||||||
|
print_commands()
|
||||||
|
return
|
||||||
|
|
||||||
|
print(json.dumps(getattr(client, command)(*args["command_arguments"])))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Main entry point for the client
|
||||||
|
"""
|
||||||
|
args = add_arguments()
|
||||||
|
main_exec(args)
|
||||||
Executable
+424
@@ -0,0 +1,424 @@
|
|||||||
|
"""
|
||||||
|
A Rest Client for Freqtrade bot
|
||||||
|
|
||||||
|
Should not import anything from freqtrade,
|
||||||
|
so it can be used as a standalone script, and can be installed independently.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("ft_rest_client")
|
||||||
|
|
||||||
|
|
||||||
|
class FtRestClient:
|
||||||
|
|
||||||
|
def __init__(self, serverurl, username=None, password=None, *,
|
||||||
|
pool_connections=10, pool_maxsize=10):
|
||||||
|
|
||||||
|
self._serverurl = serverurl
|
||||||
|
self._session = requests.Session()
|
||||||
|
|
||||||
|
# allow configuration of pool
|
||||||
|
adapter = requests.adapters.HTTPAdapter(
|
||||||
|
pool_connections=pool_connections,
|
||||||
|
pool_maxsize=pool_maxsize
|
||||||
|
)
|
||||||
|
self._session.mount('http://', adapter)
|
||||||
|
|
||||||
|
self._session.auth = (username, password)
|
||||||
|
|
||||||
|
def _call(self, method, apipath, params: Optional[dict] = None, data=None, files=None):
|
||||||
|
|
||||||
|
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
|
||||||
|
raise ValueError(f'invalid method <{method}>')
|
||||||
|
basepath = f"{self._serverurl}/api/v1/{apipath}"
|
||||||
|
|
||||||
|
hd = {"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Split url
|
||||||
|
schema, netloc, path, par, query, fragment = urlparse(basepath)
|
||||||
|
# URLEncode query string
|
||||||
|
query = urlencode(params) if params else ""
|
||||||
|
# recombine url
|
||||||
|
url = urlunparse((schema, netloc, path, par, query, fragment))
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = self._session.request(method, url, headers=hd, data=json.dumps(data))
|
||||||
|
# return resp.text
|
||||||
|
return resp.json()
|
||||||
|
except ConnectionError:
|
||||||
|
logger.warning("Connection error")
|
||||||
|
|
||||||
|
def _get(self, apipath, params: Optional[dict] = None):
|
||||||
|
return self._call("GET", apipath, params=params)
|
||||||
|
|
||||||
|
def _delete(self, apipath, params: Optional[dict] = None):
|
||||||
|
return self._call("DELETE", apipath, params=params)
|
||||||
|
|
||||||
|
def _post(self, apipath, params: Optional[dict] = None, data: Optional[dict] = None):
|
||||||
|
return self._call("POST", apipath, params=params, data=data)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the bot if it's in the stopped state.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._post("start")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the bot. Use `start` to restart.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._post("stop")
|
||||||
|
|
||||||
|
def stopbuy(self):
|
||||||
|
"""Stop buying (but handle sells gracefully). Use `reload_config` to reset.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._post("stopbuy")
|
||||||
|
|
||||||
|
def reload_config(self):
|
||||||
|
"""Reload configuration.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._post("reload_config")
|
||||||
|
|
||||||
|
def balance(self):
|
||||||
|
"""Get the account balance.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("balance")
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
"""Return the amount of open trades.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("count")
|
||||||
|
|
||||||
|
def entries(self, pair=None):
|
||||||
|
"""Returns List of dicts containing all Trades, based on buy tag performance
|
||||||
|
Can either be average for all pairs or a specific pair provided
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("entries", params={"pair": pair} if pair else None)
|
||||||
|
|
||||||
|
def exits(self, pair=None):
|
||||||
|
"""Returns List of dicts containing all Trades, based on exit reason performance
|
||||||
|
Can either be average for all pairs or a specific pair provided
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("exits", params={"pair": pair} if pair else None)
|
||||||
|
|
||||||
|
def mix_tags(self, pair=None):
|
||||||
|
"""Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
|
||||||
|
Can either be average for all pairs or a specific pair provided
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("mix_tags", params={"pair": pair} if pair else None)
|
||||||
|
|
||||||
|
def locks(self):
|
||||||
|
"""Return current locks
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("locks")
|
||||||
|
|
||||||
|
def delete_lock(self, lock_id):
|
||||||
|
"""Delete (disable) lock from the database.
|
||||||
|
|
||||||
|
:param lock_id: ID for the lock to delete
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._delete(f"locks/{lock_id}")
|
||||||
|
|
||||||
|
def daily(self, days=None):
|
||||||
|
"""Return the profits for each day, and amount of trades.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("daily", params={"timescale": days} if days else None)
|
||||||
|
|
||||||
|
def weekly(self, weeks=None):
|
||||||
|
"""Return the profits for each week, and amount of trades.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("weekly", params={"timescale": weeks} if weeks else None)
|
||||||
|
|
||||||
|
def monthly(self, months=None):
|
||||||
|
"""Return the profits for each month, and amount of trades.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("monthly", params={"timescale": months} if months else None)
|
||||||
|
|
||||||
|
def edge(self):
|
||||||
|
"""Return information about edge.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("edge")
|
||||||
|
|
||||||
|
def profit(self):
|
||||||
|
"""Return the profit summary.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("profit")
|
||||||
|
|
||||||
|
def stats(self):
|
||||||
|
"""Return the stats report (durations, sell-reasons).
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("stats")
|
||||||
|
|
||||||
|
def performance(self):
|
||||||
|
"""Return the performance of the different coins.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("performance")
|
||||||
|
|
||||||
|
def status(self):
|
||||||
|
"""Get the status of open trades.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("status")
|
||||||
|
|
||||||
|
def version(self):
|
||||||
|
"""Return the version of the bot.
|
||||||
|
|
||||||
|
:return: json object containing the version
|
||||||
|
"""
|
||||||
|
return self._get("version")
|
||||||
|
|
||||||
|
def show_config(self):
|
||||||
|
""" Returns part of the configuration, relevant for trading operations.
|
||||||
|
:return: json object containing the version
|
||||||
|
"""
|
||||||
|
return self._get("show_config")
|
||||||
|
|
||||||
|
def ping(self):
|
||||||
|
"""simple ping"""
|
||||||
|
configstatus = self.show_config()
|
||||||
|
if not configstatus:
|
||||||
|
return {"status": "not_running"}
|
||||||
|
elif configstatus['state'] == "running":
|
||||||
|
return {"status": "pong"}
|
||||||
|
else:
|
||||||
|
return {"status": "not_running"}
|
||||||
|
|
||||||
|
def logs(self, limit=None):
|
||||||
|
"""Show latest logs.
|
||||||
|
|
||||||
|
:param limit: Limits log messages to the last <limit> logs. No limit to get the entire log.
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("logs", params={"limit": limit} if limit else 0)
|
||||||
|
|
||||||
|
def trades(self, limit=None, offset=None):
|
||||||
|
"""Return trades history, sorted by id
|
||||||
|
|
||||||
|
:param limit: Limits trades to the X last trades. Max 500 trades.
|
||||||
|
:param offset: Offset by this amount of trades.
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
params = {}
|
||||||
|
if limit:
|
||||||
|
params['limit'] = limit
|
||||||
|
if offset:
|
||||||
|
params['offset'] = offset
|
||||||
|
return self._get("trades", params)
|
||||||
|
|
||||||
|
def trade(self, trade_id):
|
||||||
|
"""Return specific trade
|
||||||
|
|
||||||
|
:param trade_id: Specify which trade to get.
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get(f"trade/{trade_id}")
|
||||||
|
|
||||||
|
def delete_trade(self, trade_id):
|
||||||
|
"""Delete trade from the database.
|
||||||
|
Tries to close open orders. Requires manual handling of this asset on the exchange.
|
||||||
|
|
||||||
|
:param trade_id: Deletes the trade with this ID from the database.
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._delete(f"trades/{trade_id}")
|
||||||
|
|
||||||
|
def cancel_open_order(self, trade_id):
|
||||||
|
"""Cancel open order for trade.
|
||||||
|
|
||||||
|
:param trade_id: Cancels open orders for this trade.
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._delete(f"trades/{trade_id}/open-order")
|
||||||
|
|
||||||
|
def whitelist(self):
|
||||||
|
"""Show the current whitelist.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("whitelist")
|
||||||
|
|
||||||
|
def blacklist(self, *args):
|
||||||
|
"""Show the current blacklist.
|
||||||
|
|
||||||
|
:param add: List of coins to add (example: "BNB/BTC")
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
if not args:
|
||||||
|
return self._get("blacklist")
|
||||||
|
else:
|
||||||
|
return self._post("blacklist", data={"blacklist": args})
|
||||||
|
|
||||||
|
def forcebuy(self, pair, price=None):
|
||||||
|
"""Buy an asset.
|
||||||
|
|
||||||
|
:param pair: Pair to buy (ETH/BTC)
|
||||||
|
:param price: Optional - price to buy
|
||||||
|
:return: json object of the trade
|
||||||
|
"""
|
||||||
|
data = {"pair": pair,
|
||||||
|
"price": price
|
||||||
|
}
|
||||||
|
return self._post("forcebuy", data=data)
|
||||||
|
|
||||||
|
def forceenter(self, pair, side, price=None):
|
||||||
|
"""Force entering a trade
|
||||||
|
|
||||||
|
:param pair: Pair to buy (ETH/BTC)
|
||||||
|
:param side: 'long' or 'short'
|
||||||
|
:param price: Optional - price to buy
|
||||||
|
:return: json object of the trade
|
||||||
|
"""
|
||||||
|
data = {"pair": pair,
|
||||||
|
"side": side,
|
||||||
|
}
|
||||||
|
if price:
|
||||||
|
data['price'] = price
|
||||||
|
return self._post("forceenter", data=data)
|
||||||
|
|
||||||
|
def forceexit(self, tradeid, ordertype=None, amount=None):
|
||||||
|
"""Force-exit a trade.
|
||||||
|
|
||||||
|
:param tradeid: Id of the trade (can be received via status command)
|
||||||
|
:param ordertype: Order type to use (must be market or limit)
|
||||||
|
:param amount: Amount to sell. Full sell if not given
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._post("forceexit", data={
|
||||||
|
"tradeid": tradeid,
|
||||||
|
"ordertype": ordertype,
|
||||||
|
"amount": amount,
|
||||||
|
})
|
||||||
|
|
||||||
|
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 pairlists_available(self):
|
||||||
|
"""Lists available pairlist providers
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("pairlists/available")
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"pair": pair,
|
||||||
|
"timeframe": timeframe,
|
||||||
|
}
|
||||||
|
if limit:
|
||||||
|
params['limit'] = limit
|
||||||
|
return self._get("pair_candles", params=params)
|
||||||
|
|
||||||
|
def pair_history(self, pair, timeframe, strategy, timerange=None, freqaimodel=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 freqaimodel: FreqAI model to use for analysis
|
||||||
|
: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,
|
||||||
|
"freqaimodel": freqaimodel,
|
||||||
|
"timerange": timerange if timerange else '',
|
||||||
|
})
|
||||||
|
|
||||||
|
def sysinfo(self):
|
||||||
|
"""Provides system information (CPU, RAM usage)
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("sysinfo")
|
||||||
|
|
||||||
|
def health(self):
|
||||||
|
"""Provides a quick health check of the running bot.
|
||||||
|
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("health")
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools >= 64.0.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "freqtrade-client"
|
||||||
|
dynamic = ["version"]
|
||||||
|
|
||||||
|
authors = [
|
||||||
|
{name = "Freqtrade Team"},
|
||||||
|
{name = "Freqtrade Team", email = "freqtrade@protonmail.com"},
|
||||||
|
]
|
||||||
|
|
||||||
|
description = "Freqtrade - Client scripts"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
license = {text = "GPLv3"}
|
||||||
|
# license = "GPLv3"
|
||||||
|
classifiers = [
|
||||||
|
"Environment :: Console",
|
||||||
|
"Intended Audience :: Science/Research",
|
||||||
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Operating System :: MacOS",
|
||||||
|
"Operating System :: Unix",
|
||||||
|
"Topic :: Office/Business :: Financial :: Investment",
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
'requests >= 2.26.0',
|
||||||
|
'python-rapidjson >= 1.0',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/freqtrade/freqtrade"
|
||||||
|
Documentation = "https://freqtrade.io"
|
||||||
|
"Bug Tracker" = "https://github.com/freqtrade/freqtrade/issues"
|
||||||
|
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
freqtrade-client = "freqtrade_client.ft_client:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["freqtrade_client*"]
|
||||||
|
exclude = ["tests", "tests.*"]
|
||||||
|
namespaces = true
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "freqtrade_client.__version__"}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Requirements for freqtrade client library
|
||||||
|
requests==2.31.0
|
||||||
|
python-rapidjson==1.16
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import re
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from freqtrade_client import FtRestClient
|
||||||
|
from freqtrade_client.ft_client import add_arguments, main_exec
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
|
||||||
|
def log_has_re(line, logs):
|
||||||
|
"""Check if line matches some caplog's message."""
|
||||||
|
return any(re.match(line, message) for message in logs.messages)
|
||||||
|
|
||||||
|
|
||||||
|
def get_rest_client():
|
||||||
|
client = FtRestClient('http://localhost:8080', 'freqtrader', 'password')
|
||||||
|
client._session = MagicMock()
|
||||||
|
request_mock = MagicMock()
|
||||||
|
client._session.request = request_mock
|
||||||
|
return client, request_mock
|
||||||
|
|
||||||
|
|
||||||
|
def test_FtRestClient_init():
|
||||||
|
client = FtRestClient('http://localhost:8080', 'freqtrader', 'password')
|
||||||
|
assert client is not None
|
||||||
|
assert client._serverurl == 'http://localhost:8080'
|
||||||
|
assert client._session is not None
|
||||||
|
assert client._session.auth is not None
|
||||||
|
assert client._session.auth == ('freqtrader', 'password')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('method', ['GET', 'POST', 'DELETE'])
|
||||||
|
def test_FtRestClient_call(method):
|
||||||
|
client, mock = get_rest_client()
|
||||||
|
client._call(method, '/dummytest')
|
||||||
|
assert mock.call_count == 1
|
||||||
|
|
||||||
|
getattr(client, f"_{method.lower()}")('/dummytest')
|
||||||
|
assert mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_FtRestClient_call_invalid(caplog):
|
||||||
|
client, _ = get_rest_client()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
client._call('PUTTY', '/dummytest')
|
||||||
|
|
||||||
|
client._session.request = MagicMock(side_effect=ConnectionError())
|
||||||
|
client._call('GET', '/dummytest')
|
||||||
|
|
||||||
|
assert log_has_re('Connection error', caplog)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('method,args', [
|
||||||
|
('start', []),
|
||||||
|
('stop', []),
|
||||||
|
('stopbuy', []),
|
||||||
|
('reload_config', []),
|
||||||
|
('balance', []),
|
||||||
|
('count', []),
|
||||||
|
('entries', []),
|
||||||
|
('exits', []),
|
||||||
|
('mix_tags', []),
|
||||||
|
('locks', []),
|
||||||
|
('delete_lock', [2]),
|
||||||
|
('daily', []),
|
||||||
|
('daily', [15]),
|
||||||
|
('weekly', []),
|
||||||
|
('weekly', [15]),
|
||||||
|
('monthly', []),
|
||||||
|
('monthly', [12]),
|
||||||
|
('edge', []),
|
||||||
|
('profit', []),
|
||||||
|
('stats', []),
|
||||||
|
('performance', []),
|
||||||
|
('status', []),
|
||||||
|
('version', []),
|
||||||
|
('show_config', []),
|
||||||
|
('ping', []),
|
||||||
|
('logs', []),
|
||||||
|
('logs', [55]),
|
||||||
|
('trades', []),
|
||||||
|
('trades', [5]),
|
||||||
|
('trades', [5, 5]), # With offset
|
||||||
|
('trade', [1]),
|
||||||
|
('delete_trade', [1]),
|
||||||
|
('cancel_open_order', [1]),
|
||||||
|
('whitelist', []),
|
||||||
|
('blacklist', []),
|
||||||
|
('blacklist', ['XRP/USDT']),
|
||||||
|
('blacklist', ['XRP/USDT', 'BTC/USDT']),
|
||||||
|
('forcebuy', ['XRP/USDT']),
|
||||||
|
('forcebuy', ['XRP/USDT', 1.5]),
|
||||||
|
('forceenter', ['XRP/USDT', 'short']),
|
||||||
|
('forceenter', ['XRP/USDT', 'short', 1.5]),
|
||||||
|
('forceexit', [1]),
|
||||||
|
('forceexit', [1, 'limit']),
|
||||||
|
('forceexit', [1, 'limit', 100]),
|
||||||
|
('strategies', []),
|
||||||
|
('strategy', ['sampleStrategy']),
|
||||||
|
('pairlists_available', []),
|
||||||
|
('plot_config', []),
|
||||||
|
('available_pairs', []),
|
||||||
|
('available_pairs', ['5m']),
|
||||||
|
('pair_candles', ['XRP/USDT', '5m']),
|
||||||
|
('pair_candles', ['XRP/USDT', '5m', 500]),
|
||||||
|
('pair_history', ['XRP/USDT', '5m', 'SampleStrategy']),
|
||||||
|
('sysinfo', []),
|
||||||
|
('health', []),
|
||||||
|
])
|
||||||
|
def test_FtRestClient_call_explicit_methods(method, args):
|
||||||
|
client, mock = get_rest_client()
|
||||||
|
exec = getattr(client, method)
|
||||||
|
exec(*args)
|
||||||
|
assert mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_ft_client(mocker, capsys, caplog):
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
args = add_arguments(['-V'])
|
||||||
|
|
||||||
|
args = add_arguments(['--show'])
|
||||||
|
assert isinstance(args, dict)
|
||||||
|
assert args['show'] is True
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
main_exec(args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'Possible commands' in captured.out
|
||||||
|
|
||||||
|
mock = mocker.patch('freqtrade_client.ft_client.FtRestClient._call')
|
||||||
|
args = add_arguments([
|
||||||
|
'--config',
|
||||||
|
'tests/testdata/testconfigs/main_test_config.json',
|
||||||
|
'ping'
|
||||||
|
])
|
||||||
|
main_exec(args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert mock.call_count == 1
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
args = add_arguments(['--config', 'tests/testdata/testconfigs/nonexisting.json'])
|
||||||
|
main_exec(args)
|
||||||
|
|
||||||
|
assert log_has_re(r'Could not load config file .*nonexisting\.json\.',
|
||||||
|
caplog)
|
||||||
|
|
||||||
|
args = add_arguments([
|
||||||
|
'--config',
|
||||||
|
'tests/testdata/testconfigs/main_test_config.json',
|
||||||
|
'whatever'
|
||||||
|
])
|
||||||
|
main_exec(args)
|
||||||
|
assert log_has_re('Command whatever not defined', caplog)
|
||||||
+3
-500
@@ -7,505 +7,8 @@ Should not import anything from freqtrade,
|
|||||||
so it can be used as a standalone script.
|
so it can be used as a standalone script.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
from freqtrade_client.ft_client import main
|
||||||
import inspect
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
from urllib.parse import urlencode, urlparse, urlunparse
|
|
||||||
|
|
||||||
import rapidjson
|
|
||||||
import requests
|
|
||||||
from requests.exceptions import ConnectionError
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(
|
main()
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
)
|
|
||||||
logger = logging.getLogger("ft_rest_client")
|
|
||||||
|
|
||||||
|
|
||||||
class FtRestClient:
|
|
||||||
|
|
||||||
def __init__(self, serverurl, username=None, password=None):
|
|
||||||
|
|
||||||
self._serverurl = serverurl
|
|
||||||
self._session = requests.Session()
|
|
||||||
self._session.auth = (username, password)
|
|
||||||
|
|
||||||
def _call(self, method, apipath, params: Optional[dict] = None, data=None, files=None):
|
|
||||||
|
|
||||||
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
|
|
||||||
raise ValueError(f'invalid method <{method}>')
|
|
||||||
basepath = f"{self._serverurl}/api/v1/{apipath}"
|
|
||||||
|
|
||||||
hd = {"Accept": "application/json",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Split url
|
|
||||||
schema, netloc, path, par, query, fragment = urlparse(basepath)
|
|
||||||
# URLEncode query string
|
|
||||||
query = urlencode(params) if params else ""
|
|
||||||
# recombine url
|
|
||||||
url = urlunparse((schema, netloc, path, par, query, fragment))
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = self._session.request(method, url, headers=hd, data=json.dumps(data))
|
|
||||||
# return resp.text
|
|
||||||
return resp.json()
|
|
||||||
except ConnectionError:
|
|
||||||
logger.warning("Connection error")
|
|
||||||
|
|
||||||
def _get(self, apipath, params: Optional[dict] = None):
|
|
||||||
return self._call("GET", apipath, params=params)
|
|
||||||
|
|
||||||
def _delete(self, apipath, params: Optional[dict] = None):
|
|
||||||
return self._call("DELETE", apipath, params=params)
|
|
||||||
|
|
||||||
def _post(self, apipath, params: Optional[dict] = None, data: Optional[dict] = None):
|
|
||||||
return self._call("POST", apipath, params=params, data=data)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""Start the bot if it's in the stopped state.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._post("start")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""Stop the bot. Use `start` to restart.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._post("stop")
|
|
||||||
|
|
||||||
def stopbuy(self):
|
|
||||||
"""Stop buying (but handle sells gracefully). Use `reload_config` to reset.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._post("stopbuy")
|
|
||||||
|
|
||||||
def reload_config(self):
|
|
||||||
"""Reload configuration.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._post("reload_config")
|
|
||||||
|
|
||||||
def balance(self):
|
|
||||||
"""Get the account balance.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("balance")
|
|
||||||
|
|
||||||
def count(self):
|
|
||||||
"""Return the amount of open trades.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("count")
|
|
||||||
|
|
||||||
def entries(self, pair=None):
|
|
||||||
"""Returns List of dicts containing all Trades, based on buy tag performance
|
|
||||||
Can either be average for all pairs or a specific pair provided
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("entries", params={"pair": pair} if pair else None)
|
|
||||||
|
|
||||||
def exits(self, pair=None):
|
|
||||||
"""Returns List of dicts containing all Trades, based on exit reason performance
|
|
||||||
Can either be average for all pairs or a specific pair provided
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("exits", params={"pair": pair} if pair else None)
|
|
||||||
|
|
||||||
def mix_tags(self, pair=None):
|
|
||||||
"""Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
|
|
||||||
Can either be average for all pairs or a specific pair provided
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("mix_tags", params={"pair": pair} if pair else None)
|
|
||||||
|
|
||||||
def locks(self):
|
|
||||||
"""Return current locks
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("locks")
|
|
||||||
|
|
||||||
def delete_lock(self, lock_id):
|
|
||||||
"""Delete (disable) lock from the database.
|
|
||||||
|
|
||||||
:param lock_id: ID for the lock to delete
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._delete(f"locks/{lock_id}")
|
|
||||||
|
|
||||||
def daily(self, days=None):
|
|
||||||
"""Return the profits for each day, and amount of trades.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("daily", params={"timescale": days} if days else None)
|
|
||||||
|
|
||||||
def weekly(self, weeks=None):
|
|
||||||
"""Return the profits for each week, and amount of trades.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("weekly", params={"timescale": weeks} if weeks else None)
|
|
||||||
|
|
||||||
def monthly(self, months=None):
|
|
||||||
"""Return the profits for each month, and amount of trades.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("monthly", params={"timescale": months} if months else None)
|
|
||||||
|
|
||||||
def edge(self):
|
|
||||||
"""Return information about edge.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("edge")
|
|
||||||
|
|
||||||
def profit(self):
|
|
||||||
"""Return the profit summary.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("profit")
|
|
||||||
|
|
||||||
def stats(self):
|
|
||||||
"""Return the stats report (durations, sell-reasons).
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("stats")
|
|
||||||
|
|
||||||
def performance(self):
|
|
||||||
"""Return the performance of the different coins.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("performance")
|
|
||||||
|
|
||||||
def status(self):
|
|
||||||
"""Get the status of open trades.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("status")
|
|
||||||
|
|
||||||
def version(self):
|
|
||||||
"""Return the version of the bot.
|
|
||||||
|
|
||||||
:return: json object containing the version
|
|
||||||
"""
|
|
||||||
return self._get("version")
|
|
||||||
|
|
||||||
def show_config(self):
|
|
||||||
""" Returns part of the configuration, relevant for trading operations.
|
|
||||||
:return: json object containing the version
|
|
||||||
"""
|
|
||||||
return self._get("show_config")
|
|
||||||
|
|
||||||
def ping(self):
|
|
||||||
"""simple ping"""
|
|
||||||
configstatus = self.show_config()
|
|
||||||
if not configstatus:
|
|
||||||
return {"status": "not_running"}
|
|
||||||
elif configstatus['state'] == "running":
|
|
||||||
return {"status": "pong"}
|
|
||||||
else:
|
|
||||||
return {"status": "not_running"}
|
|
||||||
|
|
||||||
def logs(self, limit=None):
|
|
||||||
"""Show latest logs.
|
|
||||||
|
|
||||||
:param limit: Limits log messages to the last <limit> logs. No limit to get the entire log.
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("logs", params={"limit": limit} if limit else 0)
|
|
||||||
|
|
||||||
def trades(self, limit=None, offset=None):
|
|
||||||
"""Return trades history, sorted by id
|
|
||||||
|
|
||||||
:param limit: Limits trades to the X last trades. Max 500 trades.
|
|
||||||
:param offset: Offset by this amount of trades.
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
params = {}
|
|
||||||
if limit:
|
|
||||||
params['limit'] = limit
|
|
||||||
if offset:
|
|
||||||
params['offset'] = offset
|
|
||||||
return self._get("trades", params)
|
|
||||||
|
|
||||||
def trade(self, trade_id):
|
|
||||||
"""Return specific trade
|
|
||||||
|
|
||||||
:param trade_id: Specify which trade to get.
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get(f"trade/{trade_id}")
|
|
||||||
|
|
||||||
def delete_trade(self, trade_id):
|
|
||||||
"""Delete trade from the database.
|
|
||||||
Tries to close open orders. Requires manual handling of this asset on the exchange.
|
|
||||||
|
|
||||||
:param trade_id: Deletes the trade with this ID from the database.
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._delete(f"trades/{trade_id}")
|
|
||||||
|
|
||||||
def cancel_open_order(self, trade_id):
|
|
||||||
"""Cancel open order for trade.
|
|
||||||
|
|
||||||
:param trade_id: Cancels open orders for this trade.
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._delete(f"trades/{trade_id}/open-order")
|
|
||||||
|
|
||||||
def whitelist(self):
|
|
||||||
"""Show the current whitelist.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("whitelist")
|
|
||||||
|
|
||||||
def blacklist(self, *args):
|
|
||||||
"""Show the current blacklist.
|
|
||||||
|
|
||||||
:param add: List of coins to add (example: "BNB/BTC")
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
if not args:
|
|
||||||
return self._get("blacklist")
|
|
||||||
else:
|
|
||||||
return self._post("blacklist", data={"blacklist": args})
|
|
||||||
|
|
||||||
def forcebuy(self, pair, price=None):
|
|
||||||
"""Buy an asset.
|
|
||||||
|
|
||||||
:param pair: Pair to buy (ETH/BTC)
|
|
||||||
:param price: Optional - price to buy
|
|
||||||
:return: json object of the trade
|
|
||||||
"""
|
|
||||||
data = {"pair": pair,
|
|
||||||
"price": price
|
|
||||||
}
|
|
||||||
return self._post("forcebuy", data=data)
|
|
||||||
|
|
||||||
def forceenter(self, pair, side, price=None):
|
|
||||||
"""Force entering a trade
|
|
||||||
|
|
||||||
:param pair: Pair to buy (ETH/BTC)
|
|
||||||
:param side: 'long' or 'short'
|
|
||||||
:param price: Optional - price to buy
|
|
||||||
:return: json object of the trade
|
|
||||||
"""
|
|
||||||
data = {"pair": pair,
|
|
||||||
"side": side,
|
|
||||||
}
|
|
||||||
if price:
|
|
||||||
data['price'] = price
|
|
||||||
return self._post("forceenter", data=data)
|
|
||||||
|
|
||||||
def forceexit(self, tradeid, ordertype=None, amount=None):
|
|
||||||
"""Force-exit a trade.
|
|
||||||
|
|
||||||
:param tradeid: Id of the trade (can be received via status command)
|
|
||||||
:param ordertype: Order type to use (must be market or limit)
|
|
||||||
:param amount: Amount to sell. Full sell if not given
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._post("forceexit", data={
|
|
||||||
"tradeid": tradeid,
|
|
||||||
"ordertype": ordertype,
|
|
||||||
"amount": amount,
|
|
||||||
})
|
|
||||||
|
|
||||||
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 pairlists_available(self):
|
|
||||||
"""Lists available pairlist providers
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("pairlists/available")
|
|
||||||
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
params = {
|
|
||||||
"pair": pair,
|
|
||||||
"timeframe": timeframe,
|
|
||||||
}
|
|
||||||
if limit:
|
|
||||||
params['limit'] = limit
|
|
||||||
return self._get("pair_candles", params=params)
|
|
||||||
|
|
||||||
def pair_history(self, pair, timeframe, strategy, timerange=None, freqaimodel=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 freqaimodel: FreqAI model to use for analysis
|
|
||||||
: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,
|
|
||||||
"freqaimodel": freqaimodel,
|
|
||||||
"timerange": timerange if timerange else '',
|
|
||||||
})
|
|
||||||
|
|
||||||
def sysinfo(self):
|
|
||||||
"""Provides system information (CPU, RAM usage)
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("sysinfo")
|
|
||||||
|
|
||||||
def health(self):
|
|
||||||
"""Provides a quick health check of the running bot.
|
|
||||||
|
|
||||||
:return: json object
|
|
||||||
"""
|
|
||||||
return self._get("health")
|
|
||||||
|
|
||||||
|
|
||||||
def add_arguments():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("command",
|
|
||||||
help="Positional argument defining the command to execute.",
|
|
||||||
nargs="?"
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument('--show',
|
|
||||||
help='Show possible methods with this client',
|
|
||||||
dest='show',
|
|
||||||
action='store_true',
|
|
||||||
default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument('-c', '--config',
|
|
||||||
help='Specify configuration file (default: %(default)s). ',
|
|
||||||
dest='config',
|
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
|
||||||
default='config.json'
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument("command_arguments",
|
|
||||||
help="Positional arguments for the parameters for [command]",
|
|
||||||
nargs="*",
|
|
||||||
default=[]
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
return vars(args)
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(configfile):
|
|
||||||
file = Path(configfile)
|
|
||||||
if file.is_file():
|
|
||||||
with file.open("r") as f:
|
|
||||||
config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS |
|
|
||||||
rapidjson.PM_TRAILING_COMMAS)
|
|
||||||
return config
|
|
||||||
else:
|
|
||||||
logger.warning(f"Could not load config file {file}.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def print_commands():
|
|
||||||
# Print dynamic help for the different commands using the commands doc-strings
|
|
||||||
client = FtRestClient(None)
|
|
||||||
print("Possible commands:\n")
|
|
||||||
for x, y in inspect.getmembers(client):
|
|
||||||
if not x.startswith('_'):
|
|
||||||
doc = re.sub(':return:.*', '', getattr(client, x).__doc__, flags=re.MULTILINE).rstrip()
|
|
||||||
print(f"{x}\n\t{doc}\n")
|
|
||||||
|
|
||||||
|
|
||||||
def main(args):
|
|
||||||
|
|
||||||
if args.get("show"):
|
|
||||||
print_commands()
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
config = load_config(args['config'])
|
|
||||||
url = config.get('api_server', {}).get('listen_ip_address', '127.0.0.1')
|
|
||||||
port = config.get('api_server', {}).get('listen_port', '8080')
|
|
||||||
username = config.get('api_server', {}).get('username')
|
|
||||||
password = config.get('api_server', {}).get('password')
|
|
||||||
|
|
||||||
server_url = f"http://{url}:{port}"
|
|
||||||
client = FtRestClient(server_url, username, password)
|
|
||||||
|
|
||||||
m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')]
|
|
||||||
command = args["command"]
|
|
||||||
if command not in m:
|
|
||||||
logger.error(f"Command {command} not defined")
|
|
||||||
print_commands()
|
|
||||||
return
|
|
||||||
|
|
||||||
print(json.dumps(getattr(client, command)(*args["command_arguments"])))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
args = add_arguments()
|
|
||||||
main(args)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user