Merge pull request #10015 from freqtrade/feat/clients

Add freqtrade-clients as individually installable dependency
This commit is contained in:
Matthias 2024-03-30 09:21:01 +01:00 committed by GitHub
commit f2335c5db9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 831 additions and 508 deletions

View File

@ -60,11 +60,16 @@ jobs:
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt
pip install -e ft_client/
pip install -e .
- name: Check for version alignment
run: |
python build_helpers/freqtrade_client_version_align.py
- name: Tests
run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
- name: Coveralls
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_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt
pip install -e ft_client/
pip install -e .
- name: Tests
@ -398,13 +404,14 @@ jobs:
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt
pip install -e ft_client/
pip install -e .
- name: Tests incl. ccxt compatibility tests
env:
CI_WEB_PROXY: http://152.67.78.211:13128
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
@ -467,6 +474,19 @@ jobs:
dist
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:
name: "Deploy to PyPI"
needs: [ build ]
@ -484,8 +504,10 @@ jobs:
- name: Download artifact 📦
uses: actions/download-artifact@v4
with:
name: freqtrade-build
name: freqtrade*-build
path: dist
merge-multiple: true
- name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.8.14

View File

@ -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()

View File

@ -95,11 +95,13 @@ Make sure that the following 2 lines are available in your docker-compose file:
### Consuming the API
You can consume the API by using the script `scripts/rest_client.py`.
The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system.
You can consume the API by using `freqtrade-client` (also available as `scripts/rest_client.py`).
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
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.
@ -120,7 +122,7 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
```
``` bash
python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters]
freqtrade-client --config rest_config.json <command> [optional parameters]
```
### 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.
``` bash
python3 scripts/rest_client.py help
freqtrade-client help
```
``` output

1
ft_client/LICENSE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE

4
ft_client/MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include LICENSE
include README.md
prune tests

7
ft_client/README.md Normal file
View File

@ -0,0 +1,7 @@
# Freqtrade Client
# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg)
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.

View File

@ -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']

View File

@ -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)

View File

@ -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")

54
ft_client/pyproject.toml Normal file
View File

@ -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__"}

View File

@ -0,0 +1,3 @@
# Requirements for freqtrade client library
requests==2.31.0
python-rapidjson==1.16

View File

View File

@ -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)

View File

@ -7,505 +7,8 @@ Should not import anything from freqtrade,
so it can be used as a standalone script.
"""
import argparse
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
from freqtrade_client.ft_client import main
import rapidjson
import requests
from requests.exceptions import ConnectionError
logging.basicConfig(
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)
if __name__ == '__main__':
main()

View File

@ -112,6 +112,7 @@ setup(
'python-dateutil',
'pytz',
'packaging',
'freqtrade-client',
],
extras_require={
'dev': all_extra,