diff --git a/Dockerfile.technical b/Dockerfile.technical index 5339eb232..9431e72d0 100644 --- a/Dockerfile.technical +++ b/Dockerfile.technical @@ -3,4 +3,4 @@ FROM freqtradeorg/freqtrade:develop RUN apt-get update \ && apt-get -y install git \ && apt-get clean \ - && pip install git+https://github.com/berlinguyinca/technical + && pip install git+https://github.com/freqtrade/technical diff --git a/README.md b/README.md index 8f7578561..98dad1d2e 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,6 @@ The project is currently setup in two main branches: - `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. - `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. - ## A note on Binance For Binance, please add `"BNB/"` to your blacklist to avoid issues. diff --git a/bin/freqtrade b/bin/freqtrade index e7ae7a4ca..b9e3a7008 100755 --- a/bin/freqtrade +++ b/bin/freqtrade @@ -1,7 +1,13 @@ #!/usr/bin/env python3 import sys +import warnings from freqtrade.main import main, set_loggers + set_loggers() + +warnings.warn( + "Deprecated - To continue to run the bot like this, please run `pip install -e .` again.", + DeprecationWarning) main(sys.argv[1:]) diff --git a/config_full.json.example b/config_full.json.example index 4c4ad3c58..acecfb649 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -109,6 +109,13 @@ "token": "your_telegram_token", "chat_id": "your_telegram_chat_id" }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "username": "freqtrader", + "password": "SuperSecurePassword" + }, "db_url": "sqlite:///tradesv3.sqlite", "initial_state": "running", "forcebuy_enable": false, diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 000000000..939ab3f7d --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,204 @@ +# Using FreqTrade with Docker + +## Install Docker + +Start by downloading and installing Docker CE for your platform: + +* [Mac](https://docs.docker.com/docker-for-mac/install/) +* [Windows](https://docs.docker.com/docker-for-windows/install/) +* [Linux](https://docs.docker.com/install/) + +Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. + +## Download the official FreqTrade docker image + +Pull the image from docker hub. + +Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). + +```bash +docker pull freqtradeorg/freqtrade:develop +# Optionally tag the repository so the run-commands remain shorter +docker tag freqtradeorg/freqtrade:develop freqtrade +``` + +To update the image, simply run the above commands again and restart your running container. + +Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). + +### Prepare the configuration files + +Even though you will use docker, you'll still need some files from the github repository. + +#### Clone the git repository + +Linux/Mac/Windows with WSL + +```bash +git clone https://github.com/freqtrade/freqtrade.git +``` + +Windows with docker + +```bash +git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git +``` + +#### Copy `config.json.example` to `config.json` + +```bash +cd freqtrade +cp -n config.json.example config.json +``` + +> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. + +#### Create your database file + +Production + +```bash +touch tradesv3.sqlite +```` + +Dry-Run + +```bash +touch tradesv3.dryrun.sqlite +``` + +!!! Note + Make sure to use the path to this file when starting the bot in docker. + +### Build your own Docker image + +Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. + +To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. + +```bash +docker build -t freqtrade -f Dockerfile.technical . +``` + +If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: + +```bash +docker build -f Dockerfile.develop -t freqtrade-dev . +``` + +!!! Note + For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates. + +#### Verify the Docker image + +After the build process you can verify that the image was created with: + +```bash +docker images +``` + +The output should contain the freqtrade image. + +### Run the Docker image + +You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): + +```bash +docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +!!! Warning + In this example, the database will be created inside the docker instance and will be lost when you will refresh your image. + +#### Adjust timezone + +By default, the container will use UTC timezone. +Should you find this irritating please add the following to your docker commands: + +##### Linux + +``` bash +-v /etc/timezone:/etc/timezone:ro + +# Complete command: +docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +##### MacOS + +There is known issue in OSX Docker versions after 17.09.1, whereby `/etc/localtime` cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd. + +```bash +docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). + +### Run a restartable docker image + +To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). + +#### Move your config file and database + +The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden folder in your home directory. Feel free to use a different folder and replace the folder in the upcomming commands. + +```bash +mkdir ~/.freqtrade +mv config.json ~/.freqtrade +mv tradesv3.sqlite ~/.freqtrade +``` + +#### Run the docker image + +```bash +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +``` + +!!! Note + db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. + To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` + +!!! Note + All available bot command line parameters can be added to the end of the `docker run` command. + +### Monitor your Docker instance + +You can use the following commands to monitor and manage your container: + +```bash +docker logs freqtrade +docker logs -f freqtrade +docker restart freqtrade +docker stop freqtrade +docker start freqtrade +``` + +For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). + +!!! Note + You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. + +### Backtest with docker + +The following assumes that the download/setup of the docker image have been completed successfully. +Also, backtest-data should be available at `~/.freqtrade/user_data/`. + +```bash +docker run -d \ + --name freqtrade \ + -v /etc/localtime:/etc/localtime:ro \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ + freqtrade --strategy AwsomelyProfitableStrategy backtesting +``` + +Head over to the [Backtesting Documentation](backtesting.md) for more details. + +!!! Note + Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b4e42de16..79ea4771b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -122,9 +122,10 @@ So let's write the buy strategy using these values: dataframe['macd'], dataframe['macdsignal'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe diff --git a/docs/index.md b/docs/index.md index 9abc71747..9fbc0519c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,8 +21,8 @@ Freqtrade is a cryptocurrency trading bot written in Python. We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it. - ## Features + - Based on Python 3.6+: For botting on any operating system — Windows, macOS and Linux. - Persistence: Persistence is achieved through sqlite database. - Dry-run mode: Run the bot without playing money. @@ -31,17 +31,19 @@ Freqtrade is a cryptocurrency trading bot written in Python. - Edge position sizing: Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. - Whitelist crypto-currencies: Select which crypto-currency you want to trade or use dynamic whitelists based on market (pair) trade volume. - Blacklist crypto-currencies: Select which crypto-currency you want to avoid. - - Manageable via Telegram: Manage the bot with Telegram. + - Manageable via Telegram or REST APi: Manage the bot with Telegram or via the builtin REST API. - Display profit/loss in fiat: Display your profit/loss in any of 33 fiat currencies supported. - Daily summary of profit/loss: Receive the daily summary of your profit/loss. - Performance status report: Receive the performance status of your current trades. - ## Requirements + ### Up to date clock + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. ### Hardware requirements + To run this bot we recommend you a cloud instance with a minimum of: - 2GB RAM @@ -49,6 +51,7 @@ To run this bot we recommend you a cloud instance with a minimum of: - 2vCPU ### Software requirements + - Python 3.6.x - pip (pip3) - git @@ -56,12 +59,13 @@ To run this bot we recommend you a cloud instance with a minimum of: - virtualenv (Recommended) - Docker (Recommended) - ## Support + Help / Slack For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel. Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) to join Slack channel. ## Ready to try? + Begin by reading our installation guide [here](installation). diff --git a/docs/installation.md b/docs/installation.md index 11ddc010d..d215dc8d6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,58 +1,21 @@ # Installation + This page explains how to prepare your environment for running the bot. ## Prerequisite + Before running your bot in production you will need to setup few -external API. In production mode, the bot required valid Bittrex API -credentials and a Telegram bot (optional but recommended). +external API. In production mode, the bot will require valid Exchange API +credentials. We also reccomend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). - [Setup your exchange account](#setup-your-exchange-account) -- [Backtesting commands](#setup-your-telegram-bot) ### Setup your exchange account -*To be completed, please feel free to complete this section.* -### Setup your Telegram bot -The only things you need is a working Telegram bot and its API token. -Below we explain how to create your Telegram Bot, and how to get your -Telegram user id. +You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script. -### 1. Create your Telegram bot - -**1.1. Start a chat with https://telegram.me/BotFather** - -**1.2. Send the message `/newbot`. ** *BotFather response:* -``` -Alright, a new bot. How are we going to call it? Please choose a name for your bot. -``` - -**1.3. Choose the public name of your bot (e.x. `Freqtrade bot`)** -*BotFather response:* -``` -Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot. -``` -**1.4. Choose the name id of your bot (e.x "`My_own_freqtrade_bot`")** - -**1.5. Father bot will return you the token (API key)**
-Copy it and keep it you will use it for the config parameter `token`. -*BotFather response:* -```hl_lines="4" -Done! Congratulations on your new bot. You will find it at t.me/My_own_freqtrade_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this. - -Use this token to access the HTTP API: -521095879:AAEcEZEL7ADJ56FtG_qD0bQJSKETbXCBCi0 - -For a description of the Bot API, see this page: https://core.telegram.org/bots/api -``` -**1.6. Don't forget to start the conversation with your bot, by clicking /START button** - -### 2. Get your user id -**2.1. Talk to https://telegram.me/userinfobot** - -**2.2. Get your "Id", you will use it for the config parameter -`chat_id`.** -
## Quick start + Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. ```bash @@ -61,9 +24,10 @@ cd freqtrade git checkout develop ./setup.sh --install ``` + !!! Note Windows installation is explained [here](#windows). -
+ ## Easy Installation - Linux Script If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot. @@ -101,193 +65,6 @@ Config parameter is a `config.json` configurator. This script will ask you quest ------ -## Automatic Installation - Docker - -Start by downloading Docker for your platform: - -* [Mac](https://www.docker.com/products/docker#/mac) -* [Windows](https://www.docker.com/products/docker#/windows) -* [Linux](https://www.docker.com/products/docker#/linux) - -Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo. - -### 1. Prepare the Bot - -**1.1. Clone the git repository** - -Linux/Mac/Windows with WSL -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -Windows with docker -```bash -git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git -``` - -**1.2. (Optional) Checkout the develop branch** - -```bash -git checkout develop -``` - -**1.3. Go into the new directory** - -```bash -cd freqtrade -``` - -**1.4. Copy `config.json.example` to `config.json`** - -```bash -cp -n config.json.example config.json -``` - -> To edit the config please refer to the [Bot Configuration](configuration.md) page. - -**1.5. Create your database file *(optional - the bot will create it if it is missing)** - -Production - -```bash -touch tradesv3.sqlite -```` - -Dry-Run - -```bash -touch tradesv3.dryrun.sqlite -``` - -### 2. Download or build the docker image - -Either use the prebuilt image from docker hub - or build the image yourself if you would like more control on which version is used. - -Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). - -**2.1. Download the docker image** - -Pull the image from docker hub and (optionally) change the name of the image - -```bash -docker pull freqtradeorg/freqtrade:develop -# Optionally tag the repository so the run-commands remain shorter -docker tag freqtradeorg/freqtrade:develop freqtrade -``` - -To update the image, simply run the above commands again and restart your running container. - -**2.2. Build the Docker image** - -```bash -cd freqtrade -docker build -t freqtrade . -``` - -If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: - -```bash -docker build -f ./Dockerfile.develop -t freqtrade-dev . -``` - -For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates. - -### 3. Verify the Docker image - -After the build process you can verify that the image was created with: - -```bash -docker images -``` - -### 4. Run the Docker image - -You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): - -```bash -docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtime cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd. - -```bash -docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). - -In this example, the database will be created inside the docker instance and will be lost when you will refresh your image. - -### 5. Run a restartable docker image - -To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). - -**5.1. Move your config file and database** - -```bash -mkdir ~/.freqtrade -mv config.json ~/.freqtrade -mv tradesv3.sqlite ~/.freqtrade -``` - -**5.2. Run the docker image** - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy -``` - -!!! Note - db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. - To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` - -!!! Note - All command line arguments can be added to the end of the `docker run` command. - -### 6. Monitor your Docker instance - -You can then use the following commands to monitor and manage your container: - -```bash -docker logs freqtrade -docker logs -f freqtrade -docker restart freqtrade -docker stop freqtrade -docker start freqtrade -``` - -For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). - -!!! Note - You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. - -### 7. Backtest with docker - -The following assumes that the above steps (1-4) have been completed successfully. -Also, backtest-data should be available at `~/.freqtrade/user_data/`. - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ - freqtrade --strategy AwsomelyProfitableStrategy backtesting -``` - -Head over to the [Backtesting Documentation](backtesting.md) for more details. - -!!! Note - Additional parameters can be appended after the image name (`freqtrade` in the above example). - ------- - ## Custom Installation We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. @@ -413,7 +190,7 @@ If this is the first time you run the bot, ensure you are running it in Dry-run python3.6 freqtrade -c config.json ``` -*Note*: If you run the bot on a server, you should consider using [Docker](#automatic-installation---docker) a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. +*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. #### 7. [Optional] Configure `freqtrade` as a `systemd` service @@ -441,14 +218,13 @@ The `freqtrade.service.watchdog` file contains an example of the service unit co as the watchdog. !!! Note - The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a - Docker container. + The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. ------ ## Windows -We recommend that Windows users use [Docker](#docker) as this will work much easier and smoother (also more secure). +We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. If that is not available on your system, feel free to try the instructions below, which led to success for some. @@ -492,7 +268,7 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or docker first. +The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. --- diff --git a/docs/plotting.md b/docs/plotting.md index 60c642ab3..6dc3d13b1 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -1,63 +1,83 @@ # Plotting -This page explains how to plot prices, indicator, profits. + +This page explains how to plot prices, indicators and profits. ## Installation Plotting scripts use Plotly library. Install/upgrade it with: +``` bash +pip install -U -r requirements-plot.txt ``` -pip install --upgrade plotly -``` - -At least version 2.3.0 is required. ## Plot price and indicators + Usage for the price plotter: -``` -script/plot_dataframe.py [-h] [-p pairs] [--live] +``` bash +python3 script/plot_dataframe.py [-h] [-p pairs] [--live] ``` Example -``` -python scripts/plot_dataframe.py -p BTC/ETH + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH ``` -The `-p` pairs argument, can be used to specify -pairs you would like to plot. +The `-p` pairs argument can be used to specify pairs you would like to plot. -**Advanced use** +Specify custom indicators. +Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices). + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH --indicators1 sma,ema --indicators2 macd +``` + +### Advanced use To plot multiple pairs, separate them with a comma: -``` -python scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH ``` To plot the current live price use the `--live` flag: -``` -python scripts/plot_dataframe.py -p BTC/ETH --live + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH --live ``` To plot a timerange (to zoom in): + +``` bash +python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200 ``` -python scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200 -``` + Timerange doesn't work with live data. To plot trades stored in a database use `--db-url` argument: -``` -python scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH + +``` bash +python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH ``` -To plot a test strategy the strategy should have first be backtested. -The results may then be plotted with the -s argument: +To plot trades from a backtesting result, use `--export-filename ` + +``` bash +python3 scripts/plot_dataframe.py --export-filename user_data/backtest_data/backtest-result.json -p BTC/ETH ``` -python scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data// + +To plot a custom strategy the strategy should have first be backtested. +The results may then be plotted with the -s argument: + +``` bash +python3 scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data// ``` ## Plot profit -The profit plotter show a picture with three plots: +The profit plotter shows a picture with three plots: + 1) Average closing price for all pairs 2) The summarized profit made by backtesting. Note that this is not the real-world profit, but @@ -67,7 +87,7 @@ The profit plotter show a picture with three plots: The first graph is good to get a grip of how the overall market progresses. -The second graph will show how you algorithm works or doesnt. +The second graph will show how your algorithm works or doesn't. Perhaps you want an algorithm that steadily makes small profits, or one that acts less seldom, but makes big swings. @@ -76,13 +96,14 @@ that makes profit spikes. Usage for the profit plotter: -``` -script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num] +``` bash +python3 script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num] ``` The `-p` pair argument, can be used to plot a single pair Example -``` + +``` bash python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p LTC/BTC ``` diff --git a/docs/rest-api.md b/docs/rest-api.md new file mode 100644 index 000000000..0508f83e4 --- /dev/null +++ b/docs/rest-api.md @@ -0,0 +1,193 @@ +# REST API Usage + +## Configuration + +Enable the rest API by adding the api_server section to your configuration and setting `api_server.enabled` to `true`. + +Sample configuration: + +``` json + "api_server": { + "enabled": true, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "username": "Freqtrader", + "password": "SuperSecret1!" + }, +``` + +!!! Danger: Security warning + By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. + +!!! Danger: Password selection + Please make sure to select a very strong, unique password to protect your bot from unauthorized access. + +You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. + +To generate a secure password, either use a password manager, or use the below code snipped. + +``` python +import secrets +secrets.token_hex() +``` + +### Configuration with docker + +If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker. + +``` json + "api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080 + }, +``` + +Add the following to your docker command: + +``` bash + -p 127.0.0.1:8080:8080 +``` + +A complete sample-command may then look as follows: + +```bash +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -p 127.0.0.1:8080:8080 \ + freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +``` + +!!! Danger "Security warning" + By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. + +## 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. + +``` bash +python3 scripts/rest_client.py [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. + +### Minimalistic client config + +``` json +{ + "api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080 + } +} +``` + +``` bash +python3 scripts/rest_client.py --config rest_config.json [optional parameters] +``` + +## Available commands + +| Command | Default | Description | +|----------|---------|-------------| +| `start` | | Starts the trader +| `stop` | | Stops the trader +| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. +| `reload_conf` | | Reloads the configuration file +| `status` | | Lists all open trades +| `status table` | | List all open trades in a table format +| `count` | | Displays number of trades used and available +| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance +| `forcesell ` | | Instantly sells the given trade (Ignoring `minimum_roi`). +| `forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`). +| `forcebuy [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) +| `performance` | | Show performance of each finished trade grouped by pair +| `balance` | | Show account balance per currency +| `daily ` | 7 | Shows profit or loss per day, over the last n days +| `whitelist` | | Show the current whitelist +| `blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist. +| `edge` | | Show validated pairs by Edge if it is enabled. +| `version` | | Show version + +Possible commands can be listed from the rest-client script using the `help` command. + +``` bash +python3 scripts/rest_client.py help +``` + +``` output +Possible commands: +balance + Get the account balance + :returns: json object + +blacklist + Show the current blacklist + :param add: List of coins to add (example: "BNB/BTC") + :returns: json object + +count + Returns the amount of open trades + :returns: json object + +daily + Returns the amount of open trades + :returns: json object + +edge + Returns information about edge + :returns: json object + +forcebuy + Buy an asset + :param pair: Pair to buy (ETH/BTC) + :param price: Optional - price to buy + :returns: json object of the trade + +forcesell + Force-sell a trade + :param tradeid: Id of the trade (can be received via status command) + :returns: json object + +performance + Returns the performance of the different coins + :returns: json object + +profit + Returns the profit summary + :returns: json object + +reload_conf + Reload configuration + :returns: json object + +start + Start the bot if it's in stopped state. + :returns: json object + +status + Get the status of open trades + :returns: json object + +stop + Stop the bot. Use start to restart + :returns: json object + +stopbuy + Stop buying (but handle sells gracefully). + use reload_conf to reset + :returns: json object + +version + Returns the version of the bot + :returns: json object containing the version + +whitelist + Show the current whitelist + :returns: json object +``` diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 3947168c5..e06d4fdfc 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -1,10 +1,45 @@ # Telegram usage -## Prerequisite +## Setup your Telegram bot -To control your bot with Telegram, you need first to -[set up a Telegram bot](installation.md) -and add your Telegram API keys into your config file. +Below we explain how to create your Telegram Bot, and how to get your +Telegram user id. + +### 1. Create your Telegram bot + +Start a chat with the [Telegram BotFather](https://telegram.me/BotFather) + +Send the message `/newbot`. + +*BotFather response:* + +> Alright, a new bot. How are we going to call it? Please choose a name for your bot. + +Choose the public name of your bot (e.x. `Freqtrade bot`) + +*BotFather response:* + +> Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot. + +Choose the name id of your bot and send it to the BotFather (e.g. "`My_own_freqtrade_bot`") + +*BotFather response:* + +> Done! Congratulations on your new bot. You will find it at `t.me/yourbots_name_bot`. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this. + +> Use this token to access the HTTP API: `22222222:APITOKEN` + +> For a description of the Bot API, see this page: https://core.telegram.org/bots/api Father bot will return you the token (API key) + +Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it for the config parameter `token`. + +Don't forget to start the conversation with your bot, by clicking `/START` button + +### 2. Get your user id + +Talk to the [userinfobot](https://telegram.me/userinfobot) + +Get your "Id", you will use it for the config parameter `chat_id`. ## Telegram commands diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py index 7d271dfd1..97ed9ae67 100644 --- a/freqtrade/__main__.py +++ b/freqtrade/__main__.py @@ -6,10 +6,7 @@ To launch Freqtrade as a module > python -m freqtrade (with Python >= 3.6) """ -import sys - from freqtrade import main if __name__ == '__main__': - main.set_loggers() - main.main(sys.argv[1:]) + main.main() diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 327915b61..89b587c6f 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -27,7 +27,7 @@ class Arguments(object): Arguments Class. Manage the arguments received by the cli """ - def __init__(self, args: List[str], description: str) -> None: + def __init__(self, args: Optional[List[str]], description: str) -> None: self.args = args self.parsed_arg: Optional[argparse.Namespace] = None self.parser = argparse.ArgumentParser(description=description) @@ -340,25 +340,25 @@ class Arguments(object): Builds and attaches all subcommands :return: None """ - from freqtrade.optimize import backtesting, hyperopt, edge_cli + from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge subparsers = self.parser.add_subparsers(dest='subparser') # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') - backtesting_cmd.set_defaults(func=backtesting.start) + backtesting_cmd.set_defaults(func=start_backtesting) self.optimizer_shared_options(backtesting_cmd) self.backtesting_options(backtesting_cmd) # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.') - edge_cmd.set_defaults(func=edge_cli.start) + edge_cmd.set_defaults(func=start_edge) self.optimizer_shared_options(edge_cmd) self.edge_options(edge_cmd) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') - hyperopt_cmd.set_defaults(func=hyperopt.start) + hyperopt_cmd.set_defaults(func=start_hyperopt) self.optimizer_shared_options(hyperopt_cmd) self.hyperopt_options(hyperopt_cmd) @@ -405,7 +405,7 @@ class Arguments(object): raise Exception('Incorrect syntax for timerange "%s"' % text) @staticmethod - def check_int_positive(value) -> int: + def check_int_positive(value: str) -> int: try: uint = int(value) if uint <= 0: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 619508e73..4772952fc 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -156,6 +156,21 @@ CONF_SCHEMA = { 'webhookstatus': {'type': 'object'}, }, }, + 'api_server': { + 'type': 'object', + 'properties': { + 'enabled': {'type': 'boolean'}, + 'listen_ip_address': {'format': 'ipv4'}, + 'listen_port': { + 'type': 'integer', + "minimum": 1024, + "maximum": 65535 + }, + 'username': {'type': 'string'}, + 'password': {'type': 'string'}, + }, + 'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password'] + }, 'db_url': {'type': 'string'}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'forcebuy_enable': {'type': 'boolean'}, diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 86d3c3071..2dacce8c6 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -5,19 +5,21 @@ Includes: * load data for a pair (or a list of pairs) from disk * download data from exchange and store to disk """ + import logging +import operator +from datetime import datetime from pathlib import Path -from typing import Optional, List, Dict, Tuple, Any +from typing import Any, Dict, List, Optional, Tuple import arrow from pandas import DataFrame -from freqtrade import misc, OperationalException +from freqtrade import OperationalException, misc from freqtrade.arguments import TimeRange from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange import Exchange, timeframe_to_minutes - logger = logging.getLogger(__name__) @@ -63,12 +65,8 @@ def load_tickerdata_file( Load a pair from file, either .json.gz or .json :return tickerlist or None if unsuccesful """ - path = make_testdata_path(datadir) - pair_s = pair.replace('/', '_') - file = path.joinpath(f'{pair_s}-{ticker_interval}.json') - - pairdata = misc.file_load_json(file) - + filename = pair_data_filename(datadir, pair, ticker_interval) + pairdata = misc.file_load_json(filename) if not pairdata: return None @@ -124,21 +122,34 @@ def load_data(datadir: Optional[Path], refresh_pairs: bool = False, exchange: Optional[Exchange] = None, timerange: TimeRange = TimeRange(None, None, 0, 0), - fill_up_missing: bool = True) -> Dict[str, DataFrame]: + fill_up_missing: bool = True, + live: bool = False + ) -> Dict[str, DataFrame]: """ Loads ticker history data for a list of pairs the given parameters :return: dict(:) """ - result = {} + result: Dict[str, DataFrame] = {} + if live: + if exchange: + logger.info('Live: Downloading data for all defined pairs ...') + exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) + result = {key[0]: value for key, value in exchange._klines.items() if value is not None} + else: + raise OperationalException( + "Exchange needs to be initialized when using live data." + ) + else: + logger.info('Using local backtesting data ...') - for pair in pairs: - hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, - datadir=datadir, timerange=timerange, - refresh_pairs=refresh_pairs, - exchange=exchange, - fill_up_missing=fill_up_missing) - if hist is not None: - result[pair] = hist + for pair in pairs: + hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, + datadir=datadir, timerange=timerange, + refresh_pairs=refresh_pairs, + exchange=exchange, + fill_up_missing=fill_up_missing) + if hist is not None: + result[pair] = hist return result @@ -147,6 +158,13 @@ def make_testdata_path(datadir: Optional[Path]) -> Path: return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve() +def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) -> Path: + path = make_testdata_path(datadir) + pair_s = pair.replace("/", "_") + filename = path.joinpath(f'{pair_s}-{ticker_interval}.json') + return filename + + def load_cached_data_for_updating(filename: Path, ticker_interval: str, timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: @@ -209,9 +227,7 @@ def download_pair_history(datadir: Optional[Path], ) try: - path = make_testdata_path(datadir) - filepair = pair.replace("/", "_") - filename = path.joinpath(f'{filepair}-{ticker_interval}.json') + filename = pair_data_filename(datadir, pair, ticker_interval) logger.info( f'Download history data for pair: "{pair}", interval: {ticker_interval} ' @@ -236,8 +252,45 @@ def download_pair_history(datadir: Optional[Path], misc.file_dump_json(filename, data) return True - except Exception: + except Exception as e: logger.error( - f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}.' + f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}. ' + f'Error: {e}' ) return False + + +def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: + """ + Get the maximum timeframe for the given backtest data + :param data: dictionary with preprocessed backtesting data + :return: tuple containing min_date, max_date + """ + timeframe = [ + (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) + for frame in data.values() + ] + return min(timeframe, key=operator.itemgetter(0))[0], \ + max(timeframe, key=operator.itemgetter(1))[1] + + +def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, + max_date: datetime, ticker_interval_mins: int) -> bool: + """ + Validates preprocessed backtesting data for missing values and shows warnings about it that. + + :param data: dictionary with preprocessed backtesting data + :param min_date: start-date of the data + :param max_date: end-date of the data + :param ticker_interval_mins: ticker interval in minutes + """ + # total difference in minutes / interval-minutes + expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) + found_missing = False + for pair, df in data.items(): + dflen = len(df) + if dflen < expected_frames: + found_missing = True + logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values", + pair, expected_frames, dflen, expected_frames - dflen) + return found_missing diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 4801c6cb3..3ddff4772 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -13,7 +13,6 @@ from freqtrade import constants, OperationalException from freqtrade.arguments import Arguments from freqtrade.arguments import TimeRange from freqtrade.data import history -from freqtrade.optimize import get_timeframe from freqtrade.strategy.interface import SellType @@ -49,7 +48,6 @@ class Edge(): self.strategy = strategy self.ticker_interval = self.strategy.ticker_interval self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe - self.get_timeframe = get_timeframe self.advise_sell = self.strategy.advise_sell self.advise_buy = self.strategy.advise_buy @@ -117,7 +115,7 @@ class Edge(): preprocessed = self.tickerdata_to_dataframe(data) # Print timeframe - min_date, max_date = self.get_timeframe(preprocessed) + min_date, max_date = history.get_timeframe(preprocessed) logger.info( 'Measuring data from %s up to %s (%s days) ...', min_date.isoformat(), @@ -139,6 +137,7 @@ class Edge(): # If no trade found then exit if len(trades) == 0: + logger.info("No trades found.") return False # Fill missing, calculable columns, profit, duration , abs etc. diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 66857a7a5..72a0efb1f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -510,7 +510,11 @@ class Exchange(object): _LIMIT = 500 one_call = timeframe_to_msecs(ticker_interval) * _LIMIT - logger.debug("one_call: %s msecs", one_call) + logger.debug( + "one_call: %s msecs (%s)", + one_call, + arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True) + ) input_coroutines = [self._async_get_candle_history( pair, ticker_interval, since) for since in range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] @@ -541,7 +545,10 @@ class Exchange(object): or self._now_is_time_to_refresh(pair, ticker_interval)): input_coroutines.append(self._async_get_candle_history(pair, ticker_interval)) else: - logger.debug("Using cached ohlcv data for %s, %s ...", pair, ticker_interval) + logger.debug( + "Using cached ohlcv data for pair %s, interval %s ...", + pair, ticker_interval + ) tickers = asyncio.get_event_loop().run_until_complete( asyncio.gather(*input_coroutines, return_exceptions=True)) @@ -578,7 +585,11 @@ class Exchange(object): """ try: # fetch ohlcv asynchronously - logger.debug("fetching %s, %s since %s ...", pair, ticker_interval, since_ms) + s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else '' + logger.debug( + "Fetching pair %s, interval %s, since %s %s...", + pair, ticker_interval, since_ms, s + ) data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval, since=since_ms) @@ -593,7 +604,7 @@ class Exchange(object): except IndexError: logger.exception("Error loading %s. Result was %s.", pair, data) return pair, ticker_interval, [] - logger.debug("done fetching %s, %s ...", pair, ticker_interval) + logger.debug("Done fetching pair %s, interval %s ...", pair, ticker_interval) return pair, ticker_interval, data except ccxt.NotSupported as e: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 81c9dc5d3..471e9d218 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -73,7 +73,8 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] - persistence.init(self.config) + persistence.init(self.config.get('db_url', None), + clean_open_orders=self.config.get('dry_run', False)) # Set initial bot state from config initial_state = self.config.get('initial_state') diff --git a/freqtrade/main.py b/freqtrade/main.py index 877e2921d..6f073f5d4 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -3,10 +3,16 @@ Main Freqtrade bot script. Read the documentation to know what cli arguments you need. """ -import logging + import sys +# check min. python version +if sys.version_info < (3, 6): + sys.exit("Freqtrade requires Python version >= 3.6") + +# flake8: noqa E402 +import logging from argparse import Namespace -from typing import List +from typing import Any, List from freqtrade import OperationalException from freqtrade.arguments import Arguments @@ -17,37 +23,43 @@ from freqtrade.worker import Worker logger = logging.getLogger('freqtrade') -def main(sysargv: List[str]) -> None: +def main(sysargv: List[str] = None) -> None: """ This function will initiate the bot and start the trading loop. :return: None """ - arguments = Arguments( - sysargv, - 'Free, open source crypto trading bot' - ) - args: Namespace = arguments.get_parsed_arg() - - # A subcommand has been issued. - # Means if Backtesting or Hyperopt have been called we exit the bot - if hasattr(args, 'func'): - args.func(args) - return + return_code: Any = 1 worker = None - return_code = 1 try: - # Load and run worker - worker = Worker(args) - worker.run() + set_loggers() + arguments = Arguments( + sysargv, + 'Free, open source crypto trading bot' + ) + args: Namespace = arguments.get_parsed_arg() + + # A subcommand has been issued. + # Means if Backtesting or Hyperopt have been called we exit the bot + if hasattr(args, 'func'): + args.func(args) + # TODO: fetch return_code as returned by the command function here + return_code = 0 + else: + # Load and run worker + worker = Worker(args) + worker.run() + + except SystemExit as e: + return_code = e except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') return_code = 0 except OperationalException as e: logger.error(str(e)) return_code = 2 - except BaseException: + except Exception: logger.exception('Fatal exception!') finally: if worker: @@ -56,5 +68,4 @@ def main(sysargv: List[str]) -> None: if __name__ == '__main__': - set_loggers() - main(sys.argv[1:]) + main() diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 19b8dd90a..475aaa82f 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -1,49 +1,115 @@ -# pragma pylint: disable=missing-docstring - import logging -from datetime import datetime -from typing import Dict, Tuple -import operator +from argparse import Namespace +from typing import Any, Dict -import arrow -from pandas import DataFrame +from filelock import FileLock, Timeout -from freqtrade.optimize.default_hyperopt import DefaultHyperOpts # noqa: F401 +from freqtrade import DependencyException, constants +from freqtrade.configuration import Configuration +from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: +def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: """ - Get the maximum timeframe for the given backtest data - :param data: dictionary with preprocessed backtesting data - :return: tuple containing min_date, max_date + Prepare the configuration for the Hyperopt module + :param args: Cli args from Arguments() + :return: Configuration """ - timeframe = [ - (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) - for frame in data.values() - ] - return min(timeframe, key=operator.itemgetter(0))[0], \ - max(timeframe, key=operator.itemgetter(1))[1] + configuration = Configuration(args, method) + config = configuration.load_config() + + # Ensure we do not use Exchange credentials + config['exchange']['key'] = '' + config['exchange']['secret'] = '' + + if method == RunMode.BACKTEST: + if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: + raise DependencyException('stake amount could not be "%s" for backtesting' % + constants.UNLIMITED_STAKE_AMOUNT) + + if method == RunMode.HYPEROPT: + # Special cases for Hyperopt + if config.get('strategy') and config.get('strategy') != 'DefaultStrategy': + logger.error("Please don't use --strategy for hyperopt.") + logger.error( + "Read the documentation at " + "https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md " + "to understand how to configure hyperopt.") + raise DependencyException("--strategy configured but not supported for hyperopt") + + return config -def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, - max_date: datetime, ticker_interval_mins: int) -> bool: +def start_backtesting(args: Namespace) -> None: """ - Validates preprocessed backtesting data for missing values and shows warnings about it that. + Start Backtesting script + :param args: Cli args from Arguments() + :return: None + """ + # Import here to avoid loading backtesting module when it's not used + from freqtrade.optimize.backtesting import Backtesting - :param data: dictionary with preprocessed backtesting data - :param min_date: start-date of the data - :param max_date: end-date of the data - :param ticker_interval_mins: ticker interval in minutes + # Initialize configuration + config = setup_configuration(args, RunMode.BACKTEST) + + logger.info('Starting freqtrade in Backtesting mode') + + # Initialize backtesting object + backtesting = Backtesting(config) + backtesting.start() + + +def start_hyperopt(args: Namespace) -> None: """ - # total difference in minutes / interval-minutes - expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) - found_missing = False - for pair, df in data.items(): - dflen = len(df) - if dflen < expected_frames: - found_missing = True - logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values", - pair, expected_frames, dflen, expected_frames - dflen) - return found_missing + Start hyperopt script + :param args: Cli args from Arguments() + :return: None + """ + # Import here to avoid loading hyperopt module when it's not used + from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE + + # Initialize configuration + config = setup_configuration(args, RunMode.HYPEROPT) + + logger.info('Starting freqtrade in Hyperopt mode') + + lock = FileLock(HYPEROPT_LOCKFILE) + + try: + with lock.acquire(timeout=1): + + # Remove noisy log messages + logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) + logging.getLogger('filelock').setLevel(logging.WARNING) + + # Initialize backtesting object + hyperopt = Hyperopt(config) + hyperopt.start() + + except Timeout: + logger.info("Another running instance of freqtrade Hyperopt detected.") + logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " + "Hyperopt module is resource hungry. Please run your Hyperopts sequentially " + "or on separate machines.") + logger.info("Quitting now.") + # TODO: return False here in order to help freqtrade to exit + # with non-zero exit code... + # Same in Edge and Backtesting start() functions. + + +def start_edge(args: Namespace) -> None: + """ + Start Edge script + :param args: Cli args from Arguments() + :return: None + """ + from freqtrade.optimize.edge_cli import EdgeCli + # Initialize configuration + config = setup_configuration(args, RunMode.EDGE) + logger.info('Starting freqtrade in Edge mode') + + # Initialize Edge object + edge_cli = EdgeCli(config) + edge_cli.start() diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 51122cfb2..76c6556fa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -4,7 +4,6 @@ This module contains the backtesting logic """ import logging -from argparse import Namespace from copy import deepcopy from datetime import datetime, timedelta from pathlib import Path @@ -13,10 +12,7 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from tabulate import tabulate -from freqtrade import optimize -from freqtrade import DependencyException, constants from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes @@ -24,8 +20,7 @@ from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode -from freqtrade.strategy.interface import SellType, IStrategy - +from freqtrade.strategy.interface import IStrategy, SellType logger = logging.getLogger(__name__) @@ -406,24 +401,17 @@ class Backtesting(object): logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - if self.config.get('live'): - logger.info('Downloading data for all pairs in whitelist ...') - self.exchange.refresh_latest_ohlcv([(pair, self.ticker_interval) for pair in pairs]) - data = {key[0]: value for key, value in self.exchange._klines.items()} - - else: - logger.info('Using local backtesting data (using whitelist in given config) ...') - - timerange = Arguments.parse_timerange(None if self.config.get( - 'timerange') is None else str(self.config.get('timerange'))) - data = history.load_data( - datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, - pairs=pairs, - ticker_interval=self.ticker_interval, - refresh_pairs=self.config.get('refresh_pairs', False), - exchange=self.exchange, - timerange=timerange - ) + timerange = Arguments.parse_timerange(None if self.config.get( + 'timerange') is None else str(self.config.get('timerange'))) + data = history.load_data( + datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, + pairs=pairs, + ticker_interval=self.ticker_interval, + refresh_pairs=self.config.get('refresh_pairs', False), + exchange=self.exchange, + timerange=timerange, + live=self.config.get('live', False) + ) if not data: logger.critical("No data found. Terminating.") @@ -440,10 +428,10 @@ class Backtesting(object): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) - min_date, max_date = optimize.get_timeframe(data) + min_date, max_date = history.get_timeframe(data) # Validate dataframe for missing values (mainly at start and end, as fillup is called) - optimize.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes(self.ticker_interval)) + history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes(self.ticker_interval)) logger.info( 'Backtesting with data from %s up to %s (%s days)..', min_date.isoformat(), @@ -486,39 +474,3 @@ class Backtesting(object): print(' Strategy Summary '.center(133, '=')) print(self._generate_text_table_strategy(all_results)) print('\nFor more details, please look at the detail tables above') - - -def setup_configuration(args: Namespace) -> Dict[str, Any]: - """ - Prepare the configuration for the backtesting - :param args: Cli args from Arguments() - :return: Configuration - """ - configuration = Configuration(args, RunMode.BACKTEST) - config = configuration.get_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: - raise DependencyException('stake amount could not be "%s" for backtesting' % - constants.UNLIMITED_STAKE_AMOUNT) - - return config - - -def start(args: Namespace) -> None: - """ - Start Backtesting script - :param args: Cli args from Arguments() - :return: None - """ - # Initialize configuration - config = setup_configuration(args) - - logger.info('Starting freqtrade in Backtesting mode') - - # Initialize backtesting object - backtesting = Backtesting(config) - backtesting.start() diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 721848d2e..7f1cb2435 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -70,9 +70,10 @@ class DefaultHyperOpts(IHyperOpt): dataframe['close'], dataframe['sar'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe @@ -129,9 +130,10 @@ class DefaultHyperOpts(IHyperOpt): dataframe['sar'], dataframe['close'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'sell'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 return dataframe diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 9b628cf2e..8232c79c9 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -4,16 +4,13 @@ This module contains the edge backtesting interface """ import logging -from argparse import Namespace from typing import Dict, Any from tabulate import tabulate from freqtrade.edge import Edge from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver -from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -73,37 +70,7 @@ class EdgeCli(object): floatfmt=floatfmt, tablefmt="pipe") def start(self) -> None: - self.edge.calculate() - print('') # blank like for readability - print(self._generate_edge_table(self.edge._cached_pairs)) - - -def setup_configuration(args: Namespace) -> Dict[str, Any]: - """ - Prepare the configuration for edge backtesting - :param args: Cli args from Arguments() - :return: Configuration - """ - configuration = Configuration(args, RunMode.EDGECLI) - config = configuration.get_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - return config - - -def start(args: Namespace) -> None: - """ - Start Edge script - :param args: Cli args from Arguments() - :return: None - """ - # Initialize configuration - config = setup_configuration(args) - logger.info('Starting freqtrade in Edge mode') - - # Initialize Edge object - edge_cli = EdgeCli(config) - edge_cli.start() + result = self.edge.calculate() + if result: + print('') # blank line for readability + print(self._generate_edge_table(self.edge._cached_pairs)) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 92589aed2..d19d54031 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -7,28 +7,22 @@ This module contains the hyperopt logic import logging import os import sys -from argparse import Namespace from math import exp from operator import itemgetter from pathlib import Path from pprint import pprint from typing import Any, Dict, List -from filelock import Timeout, FileLock from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade import DependencyException from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration -from freqtrade.data.history import load_data +from freqtrade.data.history import load_data, get_timeframe, validate_backtest_data from freqtrade.exchange import timeframe_to_minutes -from freqtrade.optimize import get_timeframe, validate_backtest_data from freqtrade.optimize.backtesting import Backtesting -from freqtrade.state import RunMode -from freqtrade.resolvers import HyperOptResolver +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver logger = logging.getLogger(__name__) @@ -343,62 +337,3 @@ class Hyperopt(Backtesting): self.save_trials() self.log_trials_result() - - -def setup_configuration(args: Namespace) -> Dict[str, Any]: - """ - Prepare the configuration for the Hyperopt module - :param args: Cli args from Arguments() - :return: Configuration - """ - configuration = Configuration(args, RunMode.HYPEROPT) - config = configuration.load_config() - - # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - - if config.get('strategy') and config.get('strategy') != 'DefaultStrategy': - logger.error("Please don't use --strategy for hyperopt.") - logger.error( - "Read the documentation at " - "https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md " - "to understand how to configure hyperopt.") - raise DependencyException("--strategy configured but not supported for hyperopt") - - return config - - -def start(args: Namespace) -> None: - """ - Start Backtesting script - :param args: Cli args from Arguments() - :return: None - """ - # Initialize configuration - config = setup_configuration(args) - - logger.info('Starting freqtrade in Hyperopt mode') - - lock = FileLock(HYPEROPT_LOCKFILE) - - try: - with lock.acquire(timeout=1): - - # Remove noisy log messages - logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) - logging.getLogger('filelock').setLevel(logging.WARNING) - - # Initialize backtesting object - hyperopt = Hyperopt(config) - hyperopt.start() - - except Timeout: - logger.info("Another running instance of freqtrade Hyperopt detected.") - logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " - "Hyperopt module is resource hungry. Please run your Hyperopts sequentially " - "or on separate machines.") - logger.info("Quitting now.") - # TODO: return False here in order to help freqtrade to exit - # with non-zero exit code... - # Same in Edge and Backtesting start() functions. diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index ed09f6f22..c844bbc4c 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -25,15 +25,16 @@ _DECL_BASE: Any = declarative_base() _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' -def init(config: Dict) -> None: +def init(db_url: str, clean_open_orders: bool = False) -> None: """ Initializes this module with the given config, registers all known command handlers and starts polling for message updates - :param config: config to use + :param db_url: Database to use + :param clean_open_orders: Remove open orders from the database. + Useful for dry-run or if all orders have been reset on the exchange. :return: None """ - db_url = config.get('db_url', None) kwargs = {} # Take care of thread ownership if in-memory db @@ -57,7 +58,7 @@ def init(config: Dict) -> None: check_migrate(engine) # Clean dry_run DB if the db is not in-memory - if config.get('dry_run', False) and db_url != 'sqlite://': + if clean_open_orders and db_url != 'sqlite://': clean_dry_run_db() diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index 5cf6c616a..8f79349fe 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -1,5 +1,6 @@ from freqtrade.resolvers.iresolver import IResolver # noqa: F401 from freqtrade.resolvers.exchange_resolver import ExchangeResolver # noqa: F401 -from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 +# Don't import HyperoptResolver to avoid loading the whole Optimize tree +# from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401 from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401 diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py new file mode 100644 index 000000000..711202b27 --- /dev/null +++ b/freqtrade/rpc/api_server.py @@ -0,0 +1,375 @@ +import logging +import threading +from datetime import date, datetime +from ipaddress import IPv4Address +from typing import Dict + +from arrow import Arrow +from flask import Flask, jsonify, request +from flask.json import JSONEncoder +from werkzeug.serving import make_server + +from freqtrade.__init__ import __version__ +from freqtrade.rpc.rpc import RPC, RPCException + +logger = logging.getLogger(__name__) + +BASE_URI = "/api/v1" + + +class ArrowJSONEncoder(JSONEncoder): + def default(self, obj): + try: + if isinstance(obj, Arrow): + return obj.for_json() + elif isinstance(obj, date): + return obj.strftime("%Y-%m-%d") + elif isinstance(obj, datetime): + return obj.strftime("%Y-%m-%d %H:%M:%S") + iterable = iter(obj) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, obj) + + +class ApiServer(RPC): + """ + This class runs api server and provides rpc.rpc functionality to it + + This class starts a none blocking thread the api server runs within + """ + + def rpc_catch_errors(func): + + def func_wrapper(self, *args, **kwargs): + + try: + return func(self, *args, **kwargs) + except RPCException as e: + logger.exception("API Error calling %s: %s", func.__name__, e) + return self.rest_error(f"Error querying {func.__name__}: {e}") + + return func_wrapper + + def check_auth(self, username, password): + return (username == self._config['api_server'].get('username') and + password == self._config['api_server'].get('password')) + + def require_login(func): + + def func_wrapper(self, *args, **kwargs): + + auth = request.authorization + if auth and self.check_auth(auth.username, auth.password): + return func(self, *args, **kwargs) + else: + return jsonify({"error": "Unauthorized"}), 401 + + return func_wrapper + + def __init__(self, freqtrade) -> None: + """ + Init the api server, and init the super class RPC + :param freqtrade: Instance of a freqtrade bot + :return: None + """ + super().__init__(freqtrade) + + self._config = freqtrade.config + self.app = Flask(__name__) + self.app.json_encoder = ArrowJSONEncoder + + # Register application handling + self.register_rest_rpc_urls() + + thread = threading.Thread(target=self.run, daemon=True) + thread.start() + + def cleanup(self) -> None: + logger.info("Stopping API Server") + self.srv.shutdown() + + def run(self): + """ + Method that runs flask app in its own thread forever. + Section to handle configuration and running of the Rest server + also to check and warn if not bound to a loopback, warn on security risk. + """ + rest_ip = self._config['api_server']['listen_ip_address'] + rest_port = self._config['api_server']['listen_port'] + + logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') + if not IPv4Address(rest_ip).is_loopback: + logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") + logger.warning("SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json") + + if not self._config['api_server'].get('password'): + logger.warning("SECURITY WARNING - No password for local REST Server defined. " + "Please make sure that this is intentional!") + + # Run the Server + logger.info('Starting Local Rest Server.') + try: + self.srv = make_server(rest_ip, rest_port, self.app) + self.srv.serve_forever() + except Exception: + logger.exception("Api server failed to start.") + logger.info('Local Rest Server started.') + + def send_msg(self, msg: Dict[str, str]) -> None: + """ + We don't push to endpoints at the moment. + Take a look at webhooks for that functionality. + """ + pass + + def rest_dump(self, return_value): + """ Helper function to jsonify object for a webserver """ + return jsonify(return_value) + + def rest_error(self, error_msg): + return jsonify({"error": error_msg}), 502 + + def register_rest_rpc_urls(self): + """ + Registers flask app URLs that are calls to functonality in rpc.rpc. + + First two arguments passed are /URL and 'Label' + Label can be used as a shortcut when refactoring + :return: + """ + self.app.register_error_handler(404, self.page_not_found) + + # Actions to control the bot + self.app.add_url_rule(f'{BASE_URI}/start', 'start', + view_func=self._start, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy', + view_func=self._stopbuy, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/reload_conf', 'reload_conf', + view_func=self._reload_conf, methods=['POST']) + # Info commands + self.app.add_url_rule(f'{BASE_URI}/balance', 'balance', + view_func=self._balance, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/profit', 'profit', + view_func=self._profit, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/performance', 'performance', + view_func=self._performance, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/status', 'status', + view_func=self._status, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/version', 'version', + view_func=self._version, methods=['GET']) + + # Combined actions and infos + self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, + methods=['GET', 'POST']) + self.app.add_url_rule(f'{BASE_URI}/whitelist', 'whitelist', view_func=self._whitelist, + methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/forcebuy', 'forcebuy', + view_func=self._forcebuy, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/forcesell', 'forcesell', view_func=self._forcesell, + methods=['POST']) + + # TODO: Implement the following + # help (?) + + @require_login + def page_not_found(self, error): + """ + Return "404 not found", 404. + """ + return self.rest_dump({ + 'status': 'error', + 'reason': f"There's no API call for {request.base_url}.", + 'code': 404 + }), 404 + + @require_login + @rpc_catch_errors + def _start(self): + """ + Handler for /start. + Starts TradeThread in bot if stopped. + """ + msg = self._rpc_start() + return self.rest_dump(msg) + + @require_login + @rpc_catch_errors + def _stop(self): + """ + Handler for /stop. + Stops TradeThread in bot if running + """ + msg = self._rpc_stop() + return self.rest_dump(msg) + + @require_login + @rpc_catch_errors + def _stopbuy(self): + """ + Handler for /stopbuy. + Sets max_open_trades to 0 and gracefully sells all open trades + """ + msg = self._rpc_stopbuy() + return self.rest_dump(msg) + + @require_login + @rpc_catch_errors + def _version(self): + """ + Prints the bot's version + """ + return self.rest_dump({"version": __version__}) + + @require_login + @rpc_catch_errors + def _reload_conf(self): + """ + Handler for /reload_conf. + Triggers a config file reload + """ + msg = self._rpc_reload_conf() + return self.rest_dump(msg) + + @require_login + @rpc_catch_errors + def _count(self): + """ + Handler for /count. + Returns the number of trades running + """ + msg = self._rpc_count() + return self.rest_dump(msg) + + @require_login + @rpc_catch_errors + def _daily(self): + """ + Returns the last X days trading stats summary. + + :return: stats + """ + timescale = request.args.get('timescale', 7) + timescale = int(timescale) + + stats = self._rpc_daily_profit(timescale, + self._config['stake_currency'], + self._config['fiat_display_currency'] + ) + + return self.rest_dump(stats) + + @require_login + @rpc_catch_errors + def _edge(self): + """ + Returns information related to Edge. + :return: edge stats + """ + stats = self._rpc_edge() + + return self.rest_dump(stats) + + @require_login + @rpc_catch_errors + def _profit(self): + """ + Handler for /profit. + + Returns a cumulative profit statistics + :return: stats + """ + logger.info("LocalRPC - Profit Command Called") + + stats = self._rpc_trade_statistics(self._config['stake_currency'], + self._config['fiat_display_currency'] + ) + + return self.rest_dump(stats) + + @require_login + @rpc_catch_errors + def _performance(self): + """ + Handler for /performance. + + Returns a cumulative performance statistics + :return: stats + """ + logger.info("LocalRPC - performance Command Called") + + stats = self._rpc_performance() + + return self.rest_dump(stats) + + @require_login + @rpc_catch_errors + def _status(self): + """ + Handler for /status. + + Returns the current status of the trades in json format + """ + results = self._rpc_trade_status() + return self.rest_dump(results) + + @require_login + @rpc_catch_errors + def _balance(self): + """ + Handler for /balance. + + Returns the current status of the trades in json format + """ + results = self._rpc_balance(self._config.get('fiat_display_currency', '')) + return self.rest_dump(results) + + @require_login + @rpc_catch_errors + def _whitelist(self): + """ + Handler for /whitelist. + """ + results = self._rpc_whitelist() + return self.rest_dump(results) + + @require_login + @rpc_catch_errors + def _blacklist(self): + """ + Handler for /blacklist. + """ + add = request.json.get("blacklist", None) if request.method == 'POST' else None + results = self._rpc_blacklist(add) + return self.rest_dump(results) + + @require_login + @rpc_catch_errors + def _forcebuy(self): + """ + Handler for /forcebuy. + """ + asset = request.json.get("pair") + price = request.json.get("price", None) + trade = self._rpc_forcebuy(asset, price) + if trade: + return self.rest_dump(trade.to_json()) + else: + return self.rest_dump({"status": f"Error buying pair {asset}."}) + + @require_login + @rpc_catch_errors + def _forcesell(self): + """ + Handler for /forcesell. + """ + tradeid = request.json.get("tradeid") + results = self._rpc_forcesell(tradeid) + return self.rest_dump(results) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2189a0d17..048ebec63 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -48,6 +48,11 @@ class RPCException(Exception): def __str__(self): return self.message + def __json__(self): + return { + 'msg': self.message + } + class RPC(object): """ @@ -465,7 +470,7 @@ class RPC(object): } return res - def _rpc_blacklist(self, add: List[str]) -> Dict: + def _rpc_blacklist(self, add: List[str] = None) -> Dict: """ Returns the currently active blacklist""" if add: stake_currency = self._freqtrade.config.get('stake_currency') diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 7f0d0a5d4..fad532aa0 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -29,6 +29,12 @@ class RPCManager(object): from freqtrade.rpc.webhook import Webhook self.registered_modules.append(Webhook(freqtrade)) + # Enable local rest api server for cmd line control + if freqtrade.config.get('api_server', {}).get('enabled', False): + logger.info('Enabling rpc.api_server') + from freqtrade.rpc.api_server import ApiServer + self.registered_modules.append(ApiServer(freqtrade)) + def cleanup(self) -> None: """ Stops all enabled rpc modules """ logger.info('Cleaning up rpc modules ...') diff --git a/freqtrade/state.py b/freqtrade/state.py index b69c26cb5..ce2683a77 100644 --- a/freqtrade/state.py +++ b/freqtrade/state.py @@ -18,11 +18,11 @@ class State(Enum): class RunMode(Enum): """ Bot running mode (backtest, hyperopt, ...) - can be "live", "dry-run", "backtest", "edgecli", "hyperopt". + can be "live", "dry-run", "backtest", "edge", "hyperopt". """ LIVE = "live" DRY_RUN = "dry_run" BACKTEST = "backtest" - EDGECLI = "edgecli" + EDGE = "edge" HYPEROPT = "hyperopt" OTHER = "other" # Used for plotting scripts and test diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 692fda368..19b63a5d0 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -11,7 +11,7 @@ import arrow import pytest from telegram import Chat, Message, Update -from freqtrade import constants +from freqtrade import constants, persistence from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange @@ -97,7 +97,7 @@ def patch_freqtradebot(mocker, config) -> None: :return: None """ mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + persistence.init(config['db_url']) patch_exchange(mocker, None) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) @@ -113,6 +113,16 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) +def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: + """ + :param mocker: mocker to patch IStrategy class + :param value: which value IStrategy.get_signal() must return + :return: None + """ + freqtrade.strategy.get_signal = lambda e, s, t: value + freqtrade.exchange.refresh_latest_ohlcv = lambda p: None + + @pytest.fixture(autouse=True) def patch_coinmarketcap(mocker) -> None: """ @@ -963,3 +973,39 @@ def edge_conf(default_conf): } return conf + + +@pytest.fixture +def rpc_balance(): + return { + 'BTC': { + 'total': 12.0, + 'free': 12.0, + 'used': 0.0 + }, + 'ETH': { + 'total': 0.0, + 'free': 0.0, + 'used': 0.0 + }, + 'USDT': { + 'total': 10000.0, + 'free': 10000.0, + 'used': 0.0 + }, + 'LTC': { + 'total': 10.0, + 'free': 10.0, + 'used': 0.0 + }, + 'XRP': { + 'total': 1.0, + 'free': 1.0, + 'used': 0.0 + }, + 'EUR': { + 'total': 10.0, + 'free': 10.0, + 'used': 0.0 + }, + } diff --git a/freqtrade/tests/data/test_converter.py b/freqtrade/tests/data/test_converter.py index 46d564003..4c8de575d 100644 --- a/freqtrade/tests/data/test_converter.py +++ b/freqtrade/tests/data/test_converter.py @@ -2,8 +2,7 @@ import logging from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data -from freqtrade.data.history import load_pair_history -from freqtrade.optimize import validate_backtest_data, get_timeframe +from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timeframe from freqtrade.tests.conftest import log_has diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 15442f577..a13bc34af 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -2,24 +2,27 @@ import json import os -from pathlib import Path import uuid +from pathlib import Path from shutil import copyfile +from unittest.mock import MagicMock import arrow -from pandas import DataFrame import pytest +from pandas import DataFrame from freqtrade import OperationalException from freqtrade.arguments import TimeRange from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, - load_tickerdata_file, - make_testdata_path, + load_tickerdata_file, make_testdata_path, trim_tickerlist) +from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json -from freqtrade.tests.conftest import get_patched_exchange, log_has +from freqtrade.strategy.default_strategy import DefaultStrategy +from freqtrade.tests.conftest import (get_patched_exchange, log_has, + patch_exchange) # Change this if modifying UNITTEST/BTC testdatafile _BTC_UNITTEST_LENGTH = 13681 @@ -135,6 +138,31 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau _clean_test_file(file) +def test_load_data_live(default_conf, mocker, caplog) -> None: + refresh_mock = MagicMock() + mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) + exchange = get_patched_exchange(mocker, default_conf) + + history.load_data(datadir=None, ticker_interval='5m', + pairs=['UNITTEST/BTC', 'UNITTEST2/BTC'], + live=True, + exchange=exchange) + assert refresh_mock.call_count == 1 + assert len(refresh_mock.call_args_list[0][0][0]) == 2 + assert log_has('Live: Downloading data for all defined pairs ...', caplog.record_tuples) + + +def test_load_data_live_noexchange(default_conf, mocker, caplog) -> None: + + with pytest.raises(OperationalException, + match=r'Exchange needs to be initialized when using live data.'): + history.load_data(datadir=None, ticker_interval='5m', + pairs=['UNITTEST/BTC', 'UNITTEST2/BTC'], + exchange=None, + live=True, + ) + + def test_testdata_path() -> None: assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None)) @@ -321,7 +349,8 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def _clean_test_file(file1_1) _clean_test_file(file1_5) assert log_has( - 'Failed to download history data for pair: "MEME/BTC", interval: 1m.', + 'Failed to download history data for pair: "MEME/BTC", interval: 1m. ' + 'Error: File Error', caplog.record_tuples ) @@ -494,3 +523,62 @@ def test_file_dump_json_tofile() -> None: # Remove the file _clean_test_file(file) + + +def test_get_timeframe(default_conf, mocker) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + data = strategy.tickerdata_to_dataframe( + history.load_data( + datadir=None, + ticker_interval='1m', + pairs=['UNITTEST/BTC'] + ) + ) + min_date, max_date = history.get_timeframe(data) + assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' + assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' + + +def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + data = strategy.tickerdata_to_dataframe( + history.load_data( + datadir=None, + ticker_interval='1m', + pairs=['UNITTEST/BTC'], + fill_up_missing=False + ) + ) + min_date, max_date = history.get_timeframe(data) + caplog.clear() + assert history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes('1m')) + assert len(caplog.record_tuples) == 1 + assert log_has( + "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", + caplog.record_tuples) + + +def test_validate_backtest_data(default_conf, mocker, caplog) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + timerange = TimeRange('index', 'index', 200, 250) + data = strategy.tickerdata_to_dataframe( + history.load_data( + datadir=None, + ticker_interval='5m', + pairs=['UNITTEST/BTC'], + timerange=timerange + ) + ) + + min_date, max_date = history.get_timeframe(data) + caplog.clear() + assert not history.validate_backtest_data(data, min_date, max_date, + timeframe_to_minutes('5m')) + assert len(caplog.record_tuples) == 0 diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index af8674188..a14e3282e 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -10,10 +10,11 @@ import numpy as np import pytest from pandas import DataFrame, to_datetime +from freqtrade import OperationalException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import get_patched_freqtradebot +from freqtrade.tests.conftest import get_patched_freqtradebot, log_has from freqtrade.tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset) @@ -30,7 +31,50 @@ ticker_start_time = arrow.get(2018, 10, 3) ticker_interval_in_minute = 60 _ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7} +# Helpers for this test file + +def _validate_ohlc(buy_ohlc_sell_matrice): + for index, ohlc in enumerate(buy_ohlc_sell_matrice): + # if not high < open < low or not high < close < low + if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]: + raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!') + return True + + +def _build_dataframe(buy_ohlc_sell_matrice): + _validate_ohlc(buy_ohlc_sell_matrice) + tickers = [] + for ohlc in buy_ohlc_sell_matrice: + ticker = { + 'date': ticker_start_time.shift( + minutes=( + ohlc[0] * + ticker_interval_in_minute)).timestamp * + 1000, + 'buy': ohlc[1], + 'open': ohlc[2], + 'high': ohlc[3], + 'low': ohlc[4], + 'close': ohlc[5], + 'sell': ohlc[6]} + tickers.append(ticker) + + frame = DataFrame(tickers) + frame['date'] = to_datetime(frame['date'], + unit='ms', + utc=True, + infer_datetime_format=True) + + return frame + + +def _time_on_candle(number): + return np.datetime64(ticker_start_time.shift( + minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms') + + +# End helper functions # Open trade should be removed from the end tc0 = BTContainer(data=[ # D O H L C V B S @@ -203,46 +247,6 @@ def test_nonexisting_stake_amount(mocker, edge_conf): assert edge.stake_amount('N/O', 1, 2, 1) == 0.15 -def _validate_ohlc(buy_ohlc_sell_matrice): - for index, ohlc in enumerate(buy_ohlc_sell_matrice): - # if not high < open < low or not high < close < low - if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]: - raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!') - return True - - -def _build_dataframe(buy_ohlc_sell_matrice): - _validate_ohlc(buy_ohlc_sell_matrice) - tickers = [] - for ohlc in buy_ohlc_sell_matrice: - ticker = { - 'date': ticker_start_time.shift( - minutes=( - ohlc[0] * - ticker_interval_in_minute)).timestamp * - 1000, - 'buy': ohlc[1], - 'open': ohlc[2], - 'high': ohlc[3], - 'low': ohlc[4], - 'close': ohlc[5], - 'sell': ohlc[6]} - tickers.append(ticker) - - frame = DataFrame(tickers) - frame['date'] = to_datetime(frame['date'], - unit='ms', - utc=True, - infer_datetime_format=True) - - return frame - - -def _time_on_candle(number): - return np.datetime64(ticker_start_time.shift( - minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms') - - def test_edge_heartbeat_calculate(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -298,6 +302,40 @@ def test_edge_process_downloaded_data(mocker, edge_conf): assert edge._last_updated <= arrow.utcnow().timestamp + 2 +def test_edge_process_no_data(mocker, edge_conf, caplog): + edge_conf['datadir'] = None + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + assert not edge.calculate() + assert len(edge._cached_pairs) == 0 + assert log_has("No data found. Edge is stopped ...", caplog.record_tuples) + assert edge._last_updated == 0 + + +def test_edge_process_no_trades(mocker, edge_conf, caplog): + edge_conf['datadir'] = None + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.load_data', mocked_load_data) + # Return empty + mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + assert not edge.calculate() + assert len(edge._cached_pairs) == 0 + assert log_has("No trades found.", caplog.record_tuples) + + +def test_edge_init_error(mocker, edge_conf,): + edge_conf['stake_amount'] = 0.5 + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + with pytest.raises(OperationalException, match='Edge works only with unlimited stake amount'): + get_patched_freqtradebot(mocker, edge_conf) + + def test_process_expectancy(mocker, edge_conf): edge_conf['edge']['min_trade_number'] = 2 freqtrade = get_patched_freqtradebot(mocker, edge_conf) @@ -360,3 +398,11 @@ def test_process_expectancy(mocker, edge_conf): assert round(final['TEST/BTC'].risk_reward_ratio, 10) == 306.5384615384 assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0 assert round(final['TEST/BTC'].expectancy, 10) == 101.5128205128 + + # Pop last item so no trade is profitable + trades.pop() + trades_df = DataFrame(trades) + trades_df = edge._fill_calculable_fields(trades_df) + final = edge._process_expectancy(trades_df) + assert len(final) == 0 + assert isinstance(final, dict) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 924ed538f..fda9c8241 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1016,7 +1016,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) assert exchange._api_async.fetch_ohlcv.call_count == 2 - assert log_has(f"Using cached ohlcv data for {pairs[0][0]}, {pairs[0][1]} ...", + assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...", caplog.record_tuples) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index b98369533..32c6bd09b 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -2,17 +2,17 @@ import logging from unittest.mock import MagicMock -from pandas import DataFrame import pytest +from pandas import DataFrame - -from freqtrade.optimize import get_timeframe +from freqtrade.data.history import get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.strategy.interface import SellType -from freqtrade.tests.optimize import (BTrade, BTContainer, _build_backtest_dataframe, - _get_frame_time_from_offset, tests_ticker_interval) from freqtrade.tests.conftest import patch_exchange - +from freqtrade.tests.optimize import (BTContainer, BTrade, + _build_backtest_dataframe, + _get_frame_time_from_offset, + tests_ticker_interval) # Test 1 Minus 8% Close # Test with Stop-loss at 1% diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 6a39deed4..3f88a8d6c 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -17,9 +17,9 @@ from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.optimize import get_timeframe -from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, - start) +from freqtrade.data.history import get_timeframe +from freqtrade.optimize import setup_configuration, start_backtesting +from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType @@ -105,7 +105,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None: def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, - timerange=None, exchange=None): + timerange=None, exchange=None, live=False): tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', fill_missing=True)} return pairdata @@ -178,7 +178,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 'backtesting' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.BACKTEST) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -228,7 +228,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> '--export-filename', 'foo_bar.json' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.BACKTEST) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -290,7 +290,7 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog ] with pytest.raises(DependencyException, match=r'.*stake amount.*'): - setup_configuration(get_args(args)) + setup_configuration(get_args(args), RunMode.BACKTEST) def test_start(mocker, fee, default_conf, caplog) -> None: @@ -307,7 +307,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None: 'backtesting' ] args = get_args(args) - start(args) + start_backtesting(args) assert log_has( 'Starting freqtrade in Backtesting mode', caplog.record_tuples @@ -472,7 +472,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) - mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -492,7 +492,6 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: backtesting.start() # check the logs, that will contain the backtest result exists = [ - 'Using local backtesting data (using whitelist in given config) ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' @@ -507,7 +506,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) - mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -847,7 +846,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): '--disable-max-market-positions' ] args = get_args(args) - start(args) + start_backtesting(args) # check the logs, that will contain the backtest result exists = [ 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', @@ -857,7 +856,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): 'Using data folder: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Downloading data for all pairs in whitelist ...', + 'Live: Downloading data for all defined pairs ...', 'Backtesting with data from 2017-11-14T19:31:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...' @@ -901,7 +900,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): 'TestStrategy', ] args = get_args(args) - start(args) + start_backtesting(args) # 2 backtests, 4 tables assert backtestmock.call_count == 2 assert gen_table_mock.call_count == 4 @@ -916,7 +915,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): 'Using data folder: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Downloading data for all pairs in whitelist ...', + 'Live: Downloading data for all defined pairs ...', 'Backtesting with data from 2017-11-14T19:31:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 488d552c8..5d16b0f2d 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -7,7 +7,8 @@ from unittest.mock import MagicMock from freqtrade.arguments import Arguments from freqtrade.edge import PairInfo -from freqtrade.optimize.edge_cli import EdgeCli, setup_configuration, start +from freqtrade.optimize import start_edge, setup_configuration +from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange @@ -27,8 +28,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 'edge' ] - config = setup_configuration(get_args(args)) - assert config['runmode'] == RunMode.EDGECLI + config = setup_configuration(get_args(args), RunMode.EDGE) + assert config['runmode'] == RunMode.EDGE assert 'max_open_trades' in config assert 'stake_currency' in config @@ -67,14 +68,14 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N '--stoplosses=-0.01,-0.10,-0.001' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.EDGE) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config - assert config['runmode'] == RunMode.EDGECLI + assert config['runmode'] == RunMode.EDGE assert log_has( 'Using data folder: {} ...'.format(config['datadir']), caplog.record_tuples @@ -106,7 +107,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: 'edge' ] args = get_args(args) - start(args) + start_edge(args) assert log_has( 'Starting freqtrade in Edge mode', caplog.record_tuples diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index f50f58e5b..a51d74dbb 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -3,6 +3,7 @@ import json import os from datetime import datetime from unittest.mock import MagicMock +from filelock import Timeout import pandas as pd import pytest @@ -11,8 +12,9 @@ from freqtrade import DependencyException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.optimize.hyperopt import Hyperopt, setup_configuration, start -from freqtrade.resolvers import HyperOptResolver +from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE +from freqtrade.optimize import setup_configuration, start_hyperopt +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange from freqtrade.tests.optimize.test_backtesting import get_args @@ -52,7 +54,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca 'hyperopt' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.HYPEROPT) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -100,7 +102,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo '--print-all' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.HYPEROPT) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -183,7 +185,7 @@ def test_start(mocker, default_conf, caplog) -> None: '--epochs', '5' ] args = get_args(args) - start(args) + start_hyperopt(args) import pprint pprint.pprint(caplog.record_tuples) @@ -214,7 +216,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: '--epochs', '5' ] args = get_args(args) - start(args) + start_hyperopt(args) import pprint pprint.pprint(caplog.record_tuples) @@ -239,13 +241,35 @@ def test_start_failure(mocker, default_conf, caplog) -> None: ] args = get_args(args) with pytest.raises(DependencyException): - start(args) + start_hyperopt(args) assert log_has( "Please don't use --strategy for hyperopt.", caplog.record_tuples ) +def test_start_filelock(mocker, default_conf, caplog) -> None: + start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE)) + mocker.patch( + 'freqtrade.configuration.Configuration._load_config_file', + lambda *args, **kwargs: default_conf + ) + mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) + patch_exchange(mocker) + + args = [ + '--config', 'config.json', + 'hyperopt', + '--epochs', '5' + ] + args = get_args(args) + start_hyperopt(args) + assert log_has( + "Another running instance of freqtrade Hyperopt detected.", + caplog.record_tuples + ) + + def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) @@ -348,20 +372,21 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: ) patch_exchange(mocker) - default_conf.update({'config': 'config.json.example'}) - default_conf.update({'epochs': 1}) - default_conf.update({'timerange': None}) - default_conf.update({'spaces': 'all'}) - default_conf.update({'hyperopt_jobs': 1}) + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) hyperopt.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.start() parallel.assert_called_once() - - assert 'Best result:\nfoo result\nwith values:\n\n' in caplog.text + assert log_has('Best result:\nfoo result\nwith values:\n', caplog.record_tuples) assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 def test_format_results(hyperopt): diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py deleted file mode 100644 index d746aa44f..000000000 --- a/freqtrade/tests/optimize/test_optimize.py +++ /dev/null @@ -1,66 +0,0 @@ -# pragma pylint: disable=missing-docstring, protected-access, C0103 -from freqtrade import optimize -from freqtrade.arguments import TimeRange -from freqtrade.data import history -from freqtrade.exchange import timeframe_to_minutes -from freqtrade.strategy.default_strategy import DefaultStrategy -from freqtrade.tests.conftest import log_has, patch_exchange - - -def test_get_timeframe(default_conf, mocker) -> None: - patch_exchange(mocker) - strategy = DefaultStrategy(default_conf) - - data = strategy.tickerdata_to_dataframe( - history.load_data( - datadir=None, - ticker_interval='1m', - pairs=['UNITTEST/BTC'] - ) - ) - min_date, max_date = optimize.get_timeframe(data) - assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' - assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' - - -def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: - patch_exchange(mocker) - strategy = DefaultStrategy(default_conf) - - data = strategy.tickerdata_to_dataframe( - history.load_data( - datadir=None, - ticker_interval='1m', - pairs=['UNITTEST/BTC'], - fill_up_missing=False - ) - ) - min_date, max_date = optimize.get_timeframe(data) - caplog.clear() - assert optimize.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('1m')) - assert len(caplog.record_tuples) == 1 - assert log_has( - "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", - caplog.record_tuples) - - -def test_validate_backtest_data(default_conf, mocker, caplog) -> None: - patch_exchange(mocker) - strategy = DefaultStrategy(default_conf) - - timerange = TimeRange('index', 'index', 200, 250) - data = strategy.tickerdata_to_dataframe( - history.load_data( - datadir=None, - ticker_interval='5m', - pairs=['UNITTEST/BTC'], - timerange=timerange - ) - ) - - min_date, max_date = optimize.get_timeframe(data) - caplog.clear() - assert not optimize.validate_backtest_data(data, min_date, max_date, - timeframe_to_minutes('5m')) - assert len(caplog.record_tuples) == 0 diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index c3fcd62fb..5a4b5d1b2 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -14,8 +14,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.tests.conftest import patch_exchange -from freqtrade.tests.test_freqtradebot import patch_get_signal +from freqtrade.tests.conftest import patch_exchange, patch_get_signal # Functions for recurrent object patching diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py new file mode 100644 index 000000000..b7721fd8e --- /dev/null +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -0,0 +1,556 @@ +""" +Unit test file for rpc/api_server.py +""" + +from datetime import datetime +from unittest.mock import ANY, MagicMock, PropertyMock + +import pytest +from flask import Flask +from requests.auth import _basic_auth_str + +from freqtrade.__init__ import __version__ +from freqtrade.persistence import Trade +from freqtrade.rpc.api_server import BASE_URI, ApiServer +from freqtrade.state import State +from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, + patch_get_signal) + + +_TEST_USER = "FreqTrader" +_TEST_PASS = "SuperSecurePassword1!" + + +@pytest.fixture +def botclient(default_conf, mocker): + default_conf.update({"api_server": {"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080", + "username": _TEST_USER, + "password": _TEST_PASS, + }}) + + ftbot = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) + apiserver = ApiServer(ftbot) + yield ftbot, apiserver.app.test_client() + # Cleanup ... ? + + +def client_post(client, url, data={}): + return client.post(url, + content_type="application/json", + data=data, + headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)}) + + +def client_get(client, url): + return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)}) + + +def assert_response(response, expected_code=200): + assert response.status_code == expected_code + assert response.content_type == "application/json" + + +def test_api_not_found(botclient): + ftbot, client = botclient + + rc = client_post(client, f"{BASE_URI}/invalid_url") + assert_response(rc, 404) + assert rc.json == {"status": "error", + "reason": f"There's no API call for http://localhost{BASE_URI}/invalid_url.", + "code": 404 + } + + +def test_api_unauthorized(botclient): + ftbot, client = botclient + # Don't send user/pass information + rc = client.get(f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + # Change only username + ftbot.config['api_server']['username'] = "Ftrader" + rc = client_get(client, f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + # Change only password + ftbot.config['api_server']['username'] = _TEST_USER + ftbot.config['api_server']['password'] = "WrongPassword" + rc = client_get(client, f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + ftbot.config['api_server']['username'] = "Ftrader" + ftbot.config['api_server']['password'] = "WrongPassword" + + rc = client_get(client, f"{BASE_URI}/version") + assert_response(rc, 401) + assert rc.json == {'error': 'Unauthorized'} + + +def test_api_stop_workflow(botclient): + ftbot, client = botclient + assert ftbot.state == State.RUNNING + rc = client_post(client, f"{BASE_URI}/stop") + assert_response(rc) + assert rc.json == {'status': 'stopping trader ...'} + assert ftbot.state == State.STOPPED + + # Stop bot again + rc = client_post(client, f"{BASE_URI}/stop") + assert_response(rc) + assert rc.json == {'status': 'already stopped'} + + # Start bot + rc = client_post(client, f"{BASE_URI}/start") + assert_response(rc) + assert rc.json == {'status': 'starting trader ...'} + assert ftbot.state == State.RUNNING + + # Call start again + rc = client_post(client, f"{BASE_URI}/start") + assert_response(rc) + assert rc.json == {'status': 'already running'} + + +def test_api__init__(default_conf, mocker): + """ + Test __init__() method + """ + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + assert apiserver._config == default_conf + + +def test_api_run(default_conf, mocker, caplog): + default_conf.update({"api_server": {"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"}}) + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) + + server_mock = MagicMock() + mocker.patch('freqtrade.rpc.api_server.make_server', server_mock) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + + assert apiserver._config == default_conf + apiserver.run() + assert server_mock.call_count == 1 + assert server_mock.call_args_list[0][0][0] == "127.0.0.1" + assert server_mock.call_args_list[0][0][1] == "8080" + assert isinstance(server_mock.call_args_list[0][0][2], Flask) + assert hasattr(apiserver, "srv") + + assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) + assert log_has("Starting Local Rest Server.", caplog.record_tuples) + + # Test binding to public + caplog.clear() + server_mock.reset_mock() + apiserver._config.update({"api_server": {"enabled": True, + "listen_ip_address": "0.0.0.0", + "listen_port": "8089", + "password": "", + }}) + apiserver.run() + + assert server_mock.call_count == 1 + assert server_mock.call_args_list[0][0][0] == "0.0.0.0" + assert server_mock.call_args_list[0][0][1] == "8089" + assert isinstance(server_mock.call_args_list[0][0][2], Flask) + assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) + assert log_has("Starting Local Rest Server.", caplog.record_tuples) + assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", + caplog.record_tuples) + assert log_has("SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json", + caplog.record_tuples) + assert log_has("SECURITY WARNING - No password for local REST Server defined. " + "Please make sure that this is intentional!", + caplog.record_tuples) + + # Test crashing flask + caplog.clear() + mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) + apiserver.run() + assert log_has("Api server failed to start.", caplog.record_tuples) + + +def test_api_cleanup(default_conf, mocker, caplog): + default_conf.update({"api_server": {"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"}}) + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) + mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) + + apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + apiserver.run() + stop_mock = MagicMock() + stop_mock.shutdown = MagicMock() + apiserver.srv = stop_mock + + apiserver.cleanup() + assert stop_mock.shutdown.call_count == 1 + assert log_has("Stopping API Server", caplog.record_tuples) + + +def test_api_reloadconf(botclient): + ftbot, client = botclient + + rc = client_post(client, f"{BASE_URI}/reload_conf") + assert_response(rc) + assert rc.json == {'status': 'reloading config ...'} + assert ftbot.state == State.RELOAD_CONF + + +def test_api_stopbuy(botclient): + ftbot, client = botclient + assert ftbot.config['max_open_trades'] != 0 + + rc = client_post(client, f"{BASE_URI}/stopbuy") + assert_response(rc) + assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} + assert ftbot.config['max_open_trades'] == 0 + + +def test_api_balance(botclient, mocker, rpc_balance): + ftbot, client = botclient + + def mock_ticker(symbol, refresh): + if symbol == 'BTC/USDT': + return { + 'bid': 10000.00, + 'ask': 10000.00, + 'last': 10000.00, + } + elif symbol == 'XRP/BTC': + return { + 'bid': 0.00001, + 'ask': 0.00001, + 'last': 0.00001, + } + return { + 'bid': 0.1, + 'ask': 0.1, + 'last': 0.1, + } + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) + mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) + + rc = client_get(client, f"{BASE_URI}/balance") + assert_response(rc) + assert "currencies" in rc.json + assert len(rc.json["currencies"]) == 5 + assert rc.json['currencies'][0] == { + 'currency': 'BTC', + 'available': 12.0, + 'balance': 12.0, + 'pending': 0.0, + 'est_btc': 12.0, + } + + +def test_api_count(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + rc = client_get(client, f"{BASE_URI}/count") + assert_response(rc) + + assert rc.json["current"] == 0 + assert rc.json["max"] == 1.0 + + # Create some test data + ftbot.create_trade() + rc = client_get(client, f"{BASE_URI}/count") + assert_response(rc) + assert rc.json["current"] == 1.0 + assert rc.json["max"] == 1.0 + + +def test_api_daily(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + rc = client_get(client, f"{BASE_URI}/daily") + assert_response(rc) + assert len(rc.json) == 7 + assert rc.json[0][0] == str(datetime.utcnow().date()) + + +def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + rc = client_get(client, f"{BASE_URI}/edge") + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} + + +def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + + rc = client_get(client, f"{BASE_URI}/profit") + assert_response(rc, 502) + assert len(rc.json) == 1 + assert rc.json == {"error": "Error querying _profit: no closed trade"} + + ftbot.create_trade() + trade = Trade.query.first() + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + rc = client_get(client, f"{BASE_URI}/profit") + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _profit: no closed trade"} + + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + + rc = client_get(client, f"{BASE_URI}/profit") + assert_response(rc) + assert rc.json == {'avg_duration': '0:00:00', + 'best_pair': 'ETH/BTC', + 'best_rate': 6.2, + 'first_trade_date': 'just now', + 'latest_trade_date': 'just now', + 'profit_all_coin': 6.217e-05, + 'profit_all_fiat': 0, + 'profit_all_percent': 6.2, + 'profit_closed_coin': 6.217e-05, + 'profit_closed_fiat': 0, + 'profit_closed_percent': 6.2, + 'trade_count': 1 + } + + +def test_api_performance(botclient, mocker, ticker, fee): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + + trade = Trade( + pair='LTC/ETH', + amount=1, + exchange='binance', + stake_amount=1, + open_rate=0.245441, + open_order_id="123456", + is_open=False, + fee_close=fee.return_value, + fee_open=fee.return_value, + close_rate=0.265441, + + ) + trade.close_profit = trade.calc_profit_percent() + Trade.session.add(trade) + + trade = Trade( + pair='XRP/ETH', + amount=5, + stake_amount=1, + exchange='binance', + open_rate=0.412, + open_order_id="123456", + is_open=False, + fee_close=fee.return_value, + fee_open=fee.return_value, + close_rate=0.391 + ) + trade.close_profit = trade.calc_profit_percent() + Trade.session.add(trade) + Trade.session.flush() + + rc = client_get(client, f"{BASE_URI}/performance") + assert_response(rc) + assert len(rc.json) == 2 + assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, + {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}] + + +def test_api_status(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + + rc = client_get(client, f"{BASE_URI}/status") + assert_response(rc, 502) + assert rc.json == {'error': 'Error querying _status: no active trade'} + + ftbot.create_trade() + rc = client_get(client, f"{BASE_URI}/status") + assert_response(rc) + assert len(rc.json) == 1 + assert rc.json == [{'amount': 90.99181074, + 'base_currency': 'BTC', + 'close_date': None, + 'close_date_hum': None, + 'close_profit': None, + 'close_rate': None, + 'current_profit': -0.59, + 'current_rate': 1.098e-05, + 'initial_stop_loss': 0.0, + 'initial_stop_loss_pct': None, + 'open_date': ANY, + 'open_date_hum': 'just now', + 'open_order': '(limit buy rem=0.00000000)', + 'open_rate': 1.099e-05, + 'pair': 'ETH/BTC', + 'stake_amount': 0.001, + 'stop_loss': 0.0, + 'stop_loss_pct': None, + 'trade_id': 1}] + + +def test_api_version(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/version") + assert_response(rc) + assert rc.json == {"version": __version__} + + +def test_api_blacklist(botclient, mocker): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/blacklist") + assert_response(rc) + assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], + "length": 2, + "method": "StaticPairList"} + + # Add ETH/BTC to blacklist + rc = client_post(client, f"{BASE_URI}/blacklist", + data='{"blacklist": ["ETH/BTC"]}') + assert_response(rc) + assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], + "length": 3, + "method": "StaticPairList"} + + +def test_api_whitelist(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/whitelist") + assert_response(rc) + assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], + "length": 4, + "method": "StaticPairList"} + + +def test_api_forcebuy(botclient, mocker, fee): + ftbot, client = botclient + + rc = client_post(client, f"{BASE_URI}/forcebuy", + data='{"pair": "ETH/BTC"}') + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} + + # enable forcebuy + ftbot.config["forcebuy_enable"] = True + + fbuy_mock = MagicMock(return_value=None) + mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) + rc = client_post(client, f"{BASE_URI}/forcebuy", + data='{"pair": "ETH/BTC"}') + assert_response(rc) + assert rc.json == {"status": "Error buying pair ETH/BTC."} + + # Test creating trae + fbuy_mock = MagicMock(return_value=Trade( + pair='ETH/ETH', + amount=1, + exchange='bittrex', + stake_amount=1, + open_rate=0.245441, + open_order_id="123456", + open_date=datetime.utcnow(), + is_open=False, + fee_close=fee.return_value, + fee_open=fee.return_value, + close_rate=0.265441, + )) + mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) + + rc = client_post(client, f"{BASE_URI}/forcebuy", + data='{"pair": "ETH/BTC"}') + assert_response(rc) + assert rc.json == {'amount': 1, + 'close_date': None, + 'close_date_hum': None, + 'close_rate': 0.265441, + 'initial_stop_loss': None, + 'initial_stop_loss_pct': None, + 'open_date': ANY, + 'open_date_hum': 'just now', + 'open_rate': 0.245441, + 'pair': 'ETH/ETH', + 'stake_amount': 1, + 'stop_loss': None, + 'stop_loss_pct': None, + 'trade_id': None} + + +def test_api_forcesell(botclient, mocker, ticker, fee, markets): + ftbot, client = botclient + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + patch_get_signal(ftbot, (True, False)) + + rc = client_post(client, f"{BASE_URI}/forcesell", + data='{"tradeid": "1"}') + assert_response(rc, 502) + assert rc.json == {"error": "Error querying _forcesell: invalid argument"} + + ftbot.create_trade() + + rc = client_post(client, f"{BASE_URI}/forcesell", + data='{"tradeid": "1"}') + assert_response(rc) + assert rc.json == {'result': 'Created sell order for trade 1.'} diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 15d9c20c6..91fd2297f 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -135,3 +135,32 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: rpc_manager.startup_messages(default_conf, freqtradebot.pairlists) assert telegram_mock.call_count == 3 assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status'] + + +def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: + caplog.set_level(logging.DEBUG) + run_mock = MagicMock() + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) + default_conf['telegram']['enabled'] = False + rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) + + assert not log_has('Enabling rpc.api_server', caplog.record_tuples) + assert rpc_manager.registered_modules == [] + assert run_mock.call_count == 0 + + +def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: + caplog.set_level(logging.DEBUG) + run_mock = MagicMock() + mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) + + default_conf["telegram"]["enabled"] = False + default_conf["api_server"] = {"enabled": True, + "listen_ip_address": "127.0.0.1", + "listen_port": "8080"} + rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) + + assert log_has('Enabling rpc.api_server', caplog.record_tuples) + assert len(rpc_manager.registered_modules) == 1 + assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules] + assert run_mock.call_count == 1 diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 69e3006cd..46ef15f56 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -22,8 +22,7 @@ from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, - patch_exchange) -from freqtrade.tests.test_freqtradebot import patch_get_signal + patch_exchange, patch_get_signal) class DummyCls(Telegram): @@ -496,39 +495,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] -def test_telegram_balance_handle(default_conf, update, mocker) -> None: - mock_balance = { - 'BTC': { - 'total': 12.0, - 'free': 12.0, - 'used': 0.0 - }, - 'ETH': { - 'total': 0.0, - 'free': 0.0, - 'used': 0.0 - }, - 'USDT': { - 'total': 10000.0, - 'free': 10000.0, - 'used': 0.0 - }, - 'LTC': { - 'total': 10.0, - 'free': 10.0, - 'used': 0.0 - }, - 'XRP': { - 'total': 1.0, - 'free': 1.0, - 'used': 0.0 - }, - 'EUR': { - 'total': 10.0, - 'free': 10.0, - 'used': 0.0 - } - } +def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> None: def mock_ticker(symbol, refresh): if symbol == 'BTC/USDT': @@ -549,7 +516,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: 'last': 0.1, } - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance) + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) msg_mock = MagicMock() diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 0952d1c5d..ecd108b5e 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 - import argparse import pytest @@ -185,3 +184,22 @@ def test_testdata_dl_options() -> None: assert args.export == 'export/folder' assert args.days == 30 assert args.exchange == 'binance' + + +def test_check_int_positive() -> None: + + assert Arguments.check_int_positive("3") == 3 + assert Arguments.check_int_positive("1") == 1 + assert Arguments.check_int_positive("100") == 100 + + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("-2") + + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("0") + + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("3.5") + + with pytest.raises(argparse.ArgumentTypeError): + Arguments.check_int_positive("DeadBeef") diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 2587b1b36..0a6b12c3c 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -11,8 +11,8 @@ import arrow import pytest import requests -from freqtrade import (DependencyException, OperationalException, - TemporaryError, InvalidOrderException, constants) +from freqtrade import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError, constants) from freqtrade.data.dataprovider import DataProvider from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade @@ -20,7 +20,8 @@ from freqtrade.rpc import RPCMessageType from freqtrade.state import State from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge, - patch_exchange, patch_wallet) + patch_exchange, patch_get_signal, + patch_wallet) from freqtrade.worker import Worker @@ -59,16 +60,6 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) -def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: - """ - :param mocker: mocker to patch IStrategy class - :param value: which value IStrategy.get_signal() must return - :return: None - """ - freqtrade.strategy.get_signal = lambda e, s, t: value - freqtrade.exchange.refresh_latest_ohlcv = lambda p: None - - def patch_RPCManager(mocker) -> MagicMock: """ This function mock RPC manager to avoid repeating this code in almost every tests diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index e4ffc5fae..e6a2006f9 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -19,8 +19,10 @@ def test_parse_args_backtesting(mocker) -> None: Test that main() can start backtesting and also ensure we can pass some specific arguments further argument parsing is done in test_arguments.py """ - backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock()) - main(['backtesting']) + backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock()) + # it's sys.exit(0) at the end of backtesting + with pytest.raises(SystemExit): + main(['backtesting']) assert backtesting_mock.call_count == 1 call_args = backtesting_mock.call_args[0][0] assert call_args.config == ['config.json'] @@ -32,8 +34,10 @@ def test_parse_args_backtesting(mocker) -> None: def test_main_start_hyperopt(mocker) -> None: - hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) - main(['hyperopt']) + hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock()) + # it's sys.exit(0) at the end of hyperopt + with pytest.raises(SystemExit): + main(['hyperopt']) assert hyperopt_mock.call_count == 1 call_args = hyperopt_mock.call_args[0][0] assert call_args.config == ['config.json'] diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 2da6b8718..c7bcf7edf 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.misc import (common_datearray, datesarray_to_datetimearray, file_dump_json, file_load_json, format_ms_time, shorten_date) -from freqtrade.data.history import load_tickerdata_file, make_testdata_path +from freqtrade.data.history import load_tickerdata_file, pair_data_filename from freqtrade.strategy.default_strategy import DefaultStrategy @@ -60,13 +60,13 @@ def test_file_dump_json(mocker) -> None: def test_file_load_json(mocker) -> None: # 7m .json does not exist - ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-7m.json')) + ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '7m')) assert not ret # 1m json exists (but no .gz exists) - ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-1m.json')) + ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '1m')) assert ret # 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json - ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-8m.json')) + ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '8m')) assert ret diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 3312bc21d..57f054dee 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -13,12 +13,12 @@ from freqtrade.tests.conftest import log_has @pytest.fixture(scope='function') def init_persistence(default_conf): - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) def test_init_create_session(default_conf): # Check if init create a session - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert hasattr(Trade, 'session') assert 'Session' in type(Trade.session).__name__ @@ -28,7 +28,7 @@ def test_init_custom_db_url(default_conf, mocker): default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' @@ -37,7 +37,7 @@ def test_init_invalid_db_url(default_conf): # Update path to a value other than default, but still in-memory default_conf.update({'db_url': 'unknown:///some.url'}) with pytest.raises(OperationalException, match=r'.*no valid database URL*'): - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) def test_init_prod_db(default_conf, mocker): @@ -46,7 +46,7 @@ def test_init_prod_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' @@ -57,7 +57,7 @@ def test_init_dryrun_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://' @@ -336,8 +336,8 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): assert trade.calc_profit_percent(fee=0.003) == 0.06147824 +@pytest.mark.usefixtures("init_persistence") def test_clean_dry_run_db(default_conf, fee): - init(default_conf) # Simulate dry_run entries trade = Trade( @@ -424,7 +424,7 @@ def test_migrate_old(mocker, default_conf, fee): engine.execute(create_table_old) engine.execute(insert_table_old) # Run init to test migration - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -497,7 +497,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): engine.execute("create table trades_bak1 as select * from trades") # Run init to test migration - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -566,7 +566,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): engine.execute(insert_table_old) # Run init to test migration - init(default_conf) + init(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -668,8 +668,8 @@ def test_adjust_min_max_rates(fee): assert trade.min_rate == 0.96 +@pytest.mark.usefixtures("init_persistence") def test_get_open(default_conf, fee): - init(default_conf) # Simulate dry_run entries trade = Trade( @@ -713,8 +713,8 @@ def test_get_open(default_conf, fee): assert len(Trade.get_open_trades()) == 2 +@pytest.mark.usefixtures("init_persistence") def test_to_json(default_conf, fee): - init(default_conf) # Simulate dry_run entries trade = Trade( diff --git a/mkdocs.yml b/mkdocs.yml index 9932ff316..6b445ee3a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,7 @@ site_name: Freqtrade nav: - About: index.md - Installation: installation.md + - Installation Docker: docker.md - Configuration: configuration.md - Strategy Customization: strategy-customization.md - Stoploss: stoploss.md @@ -9,6 +10,7 @@ nav: - Control the bot: - Telegram: telegram-usage.md - Web Hook: webhook-config.md + - REST API: rest-api.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md - Edge positioning: edge.md diff --git a/requirements-common.txt b/requirements-common.txt index 3f755b8c0..9e854e4af 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,14 +1,14 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.18.551 -SQLAlchemy==1.3.3 +ccxt==1.18.615 +SQLAlchemy==1.3.4 python-telegram-bot==11.1.0 -arrow==0.13.2 -cachetools==3.1.0 +arrow==0.14.1 +cachetools==3.1.1 requests==2.22.0 urllib3==1.24.2 # pyup: ignore wrapt==1.11.1 -scikit-learn==0.21.1 +scikit-learn==0.21.2 joblib==0.13.2 jsonschema==3.0.1 TA-Lib==0.4.17 @@ -27,3 +27,6 @@ python-rapidjson==0.7.1 # Notify systemd sdnotify==0.3.2 + +# Api server +flask==1.0.3 diff --git a/requirements-dev.txt b/requirements-dev.txt index fa52a4869..effa714e9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,9 +4,9 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.5.0 +pytest==4.6.1 pytest-mock==1.10.4 pytest-asyncio==0.10.0 pytest-cov==2.7.1 -coveralls==1.7.0 +coveralls==1.8.0 mypy==0.701 diff --git a/requirements-plot.txt b/requirements-plot.txt index 23daee258..d4e4fc165 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==3.9.0 +plotly==3.10.0 diff --git a/requirements.txt b/requirements.txt index da87f56d9..52442fb19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Load common requirements -r requirements-common.txt -numpy==1.16.3 +numpy==1.16.4 pandas==0.24.2 scipy==1.3.0 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 7fdc607e0..4f8ffb32b 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -41,9 +41,10 @@ from freqtrade.arguments import Arguments, TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data from freqtrade.exchange import Exchange -from freqtrade.optimize.backtesting import setup_configuration +from freqtrade.optimize import setup_configuration from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver +from freqtrade.state import RunMode logger = logging.getLogger(__name__) _CONF: Dict[str, Any] = {} @@ -54,7 +55,8 @@ timeZone = pytz.UTC def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame() if args.db_url: - persistence.init(_CONF) + persistence.init(args.db_url, clean_open_orders=False) + columns = ["pair", "profit", "open_time", "close_time", "open_rate", "close_rate", "duration"] @@ -74,7 +76,7 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram file = Path(args.exportfilename) if file.exists(): - load_backtest_data(file) + trades = load_backtest_data(file) else: trades = pd.DataFrame([], columns=BT_DATA_COLUMNS) @@ -107,7 +109,7 @@ def get_trading_env(args: Namespace): global _CONF # Load the configuration - _CONF.update(setup_configuration(args)) + _CONF.update(setup_configuration(args, RunMode.BACKTEST)) print(_CONF) pairs = args.pairs.split(',') @@ -138,21 +140,15 @@ def get_tickers_data(strategy, exchange, pairs: List[str], args): ticker_interval = strategy.ticker_interval timerange = Arguments.parse_timerange(args.timerange) - tickers = {} - if args.live: - logger.info('Downloading pairs.') - exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) - for pair in pairs: - tickers[pair] = exchange.klines((pair, ticker_interval)) - else: - tickers = history.load_data( - datadir=Path(str(_CONF.get("datadir"))), - pairs=pairs, - ticker_interval=ticker_interval, - refresh_pairs=_CONF.get('refresh_pairs', False), - timerange=timerange, - exchange=Exchange(_CONF) - ) + tickers = history.load_data( + datadir=Path(str(_CONF.get("datadir"))), + pairs=pairs, + ticker_interval=ticker_interval, + refresh_pairs=_CONF.get('refresh_pairs', False), + timerange=timerange, + exchange=Exchange(_CONF), + live=args.live, + ) # No ticker found, impossible to download, len mismatch for pair, data in tickers.copy().items(): diff --git a/scripts/rest_client.py b/scripts/rest_client.py new file mode 100755 index 000000000..2261fba0b --- /dev/null +++ b/scripts/rest_client.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +""" +Simple command line client into RPC commands +Can be used as an alternate to Telegram + +Should not import anything from freqtrade, +so it can be used as a standalone script. +""" + +import argparse +import json +import logging +import inspect +from urllib.parse import urlencode, urlparse, urlunparse +from pathlib import Path + +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: dict = None, data=None, files=None): + + if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): + raise ValueError('invalid method <{0}>'.format(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: dict = None): + return self._call("GET", apipath, params=params) + + def _post(self, apipath, params: dict = None, data: dict = None): + return self._call("POST", apipath, params=params, data=data) + + def start(self): + """ + Start the bot if it's in stopped state. + :returns: json object + """ + return self._post("start") + + def stop(self): + """ + Stop the bot. Use start to restart + :returns: json object + """ + return self._post("stop") + + def stopbuy(self): + """ + Stop buying (but handle sells gracefully). + use reload_conf to reset + :returns: json object + """ + return self._post("stopbuy") + + def reload_conf(self): + """ + Reload configuration + :returns: json object + """ + return self._post("reload_conf") + + def balance(self): + """ + Get the account balance + :returns: json object + """ + return self._get("balance") + + def count(self): + """ + Returns the amount of open trades + :returns: json object + """ + return self._get("count") + + def daily(self, days=None): + """ + Returns the amount of open trades + :returns: json object + """ + return self._get("daily", params={"timescale": days} if days else None) + + def edge(self): + """ + Returns information about edge + :returns: json object + """ + return self._get("edge") + + def profit(self): + """ + Returns the profit summary + :returns: json object + """ + return self._get("profit") + + def performance(self): + """ + Returns the performance of the different coins + :returns: json object + """ + return self._get("performance") + + def status(self): + """ + Get the status of open trades + :returns: json object + """ + return self._get("status") + + def version(self): + """ + Returns the version of the bot + :returns: json object containing the version + """ + return self._get("version") + + def whitelist(self): + """ + Show the current whitelist + :returns: json object + """ + return self._get("whitelist") + + def blacklist(self, *args): + """ + Show the current blacklist + :param add: List of coins to add (example: "BNB/BTC") + :returns: 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 + :returns: json object of the trade + """ + data = {"pair": pair, + "price": price + } + return self._post("forcebuy", data=data) + + def forcesell(self, tradeid): + """ + Force-sell a trade + :param tradeid: Id of the trade (can be received via status command) + :returns: json object + """ + + return self._post("forcesell", data={"tradeid": tradeid}) + + +def add_arguments(): + parser = argparse.ArgumentParser() + parser.add_argument("command", + help="Positional argument defining the command to execute.") + + 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 = json.load(f) + return config + return {} + + +def print_commands(): + # Print dynamic help for the different commands using the commands doc-strings + client = FtRestClient(None) + print("Possible commands:") + for x, y in inspect.getmembers(client): + if not x.startswith('_'): + print(f"{x} {getattr(client, x).__doc__}") + + +def main(args): + + if args.get("help"): + print_commands() + + config = load_config(args["config"]) + url = config.get("api_server", {}).get("server_url", "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(getattr(client, command)(*args["command_arguments"])) + + +if __name__ == "__main__": + args = add_arguments() + main(args) diff --git a/setup.py b/setup.py index 35fdb2938..ca2f81d1f 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ setup(name='freqtrade', author_email='michael.egger@tsn.at', license='GPLv3', packages=['freqtrade'], - scripts=['bin/freqtrade'], setup_requires=['pytest-runner', 'numpy'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'], install_requires=[ @@ -43,6 +42,11 @@ setup(name='freqtrade', ], include_package_data=True, zip_safe=False, + entry_points={ + 'console_scripts': [ + 'freqtrade = freqtrade.main:main', + ], + }, classifiers=[ 'Programming Language :: Python :: 3.6', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index 54f65a7e6..7cb55378e 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -79,9 +79,10 @@ class SampleHyperOpts(IHyperOpt): dataframe['close'], dataframe['sar'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe @@ -138,9 +139,10 @@ class SampleHyperOpts(IHyperOpt): dataframe['sar'], dataframe['close'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'sell'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 return dataframe diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index 3cb78842f..66a5f8c09 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -51,7 +51,7 @@ class TestStrategy(IStrategy): ticker_interval = '5m' # run "populate_indicators" only for new candle - ta_on_candle = False + process_only_new_candles = False # Experimental settings (configuration will overide these if set) use_sell_signal = False