Merge branch 'develop' into rpc-refactor
This commit is contained in:
commit
3bc1b37528
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,6 +5,8 @@ config.json
|
||||
*.sqlite
|
||||
.hyperopt
|
||||
logfile.txt
|
||||
hyperopt_trials.pickle
|
||||
user_data/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@ -85,5 +87,3 @@ target/
|
||||
.venv
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
hyperopt_trials.pickle
|
||||
|
15
README.md
15
README.md
@ -136,8 +136,8 @@ to understand the requirements before sending your pull-requests.
|
||||
### Bot commands
|
||||
|
||||
```bash
|
||||
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
|
||||
[--dry-run-db]
|
||||
usage: main.py [-h] [-v] [--version] [-c PATH] [--dry-run-db] [--datadir PATH]
|
||||
[--dynamic-whitelist [INT]]
|
||||
{backtesting,hyperopt} ...
|
||||
|
||||
Simple High Frequency Trading Bot for crypto currencies
|
||||
@ -149,16 +149,17 @@ positional arguments:
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-c PATH, --config PATH
|
||||
specify configuration file (default: config.json)
|
||||
-v, --verbose be verbose
|
||||
--version show program's version number and exit
|
||||
--dynamic-whitelist [INT]
|
||||
dynamically generate and update whitelist based on 24h
|
||||
BaseVolume (Default 20 currencies)
|
||||
-c PATH, --config PATH
|
||||
specify configuration file (default: config.json)
|
||||
--dry-run-db Force dry run to use a local DB
|
||||
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
||||
only if dry_run is enabled.
|
||||
--datadir PATH path to backtest data (default freqdata/tests/testdata
|
||||
--dynamic-whitelist [INT]
|
||||
dynamically generate and update whitelist based on 24h
|
||||
BaseVolume (Default 20 currencies)
|
||||
```
|
||||
More details on:
|
||||
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
||||
|
@ -4,6 +4,7 @@
|
||||
"stake_amount": 0.05,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": false,
|
||||
"ticker_interval": 5,
|
||||
"minimal_roi": {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
|
@ -14,7 +14,7 @@ real data. This is what we call
|
||||
|
||||
Backtesting will use the crypto-currencies (pair) from your config file
|
||||
and load static tickers located in
|
||||
[/freqtrade/tests/testdata](https://github.com/gcarq/freqtrade/tree/develop/freqtrade/tests/testdata).
|
||||
[/freqtrade/tests/testdata](https://github.com/gcarq/freqtrade/tree/develop/freqtrade/tests/testdata).
|
||||
If the 5 min and 1 min ticker for the crypto-currencies to test is not
|
||||
already in the `testdata` folder, backtesting will download them
|
||||
automatically. Testdata files will not be updated until you specify it.
|
||||
@ -51,15 +51,49 @@ python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
||||
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
|
||||
```
|
||||
|
||||
**Exporting trades to file**
|
||||
```bash
|
||||
freqtrade backtesting --export trades
|
||||
```
|
||||
|
||||
**Running backtest with smaller testset**
|
||||
Use the `--timerange` argument to change how much of the testset
|
||||
you want to use. The last N ticks/timeframes will be used.
|
||||
|
||||
Example:
|
||||
```bash
|
||||
python3 ./freqtrade/main.py backtesting --timerange=-200
|
||||
```
|
||||
|
||||
***Advanced use of timerange***
|
||||
Doing `--timerange=-200` will get the last 200 timeframes
|
||||
from your inputdata. You can also specify specific dates,
|
||||
or a range span indexed by start and stop.
|
||||
|
||||
The full timerange specification:
|
||||
- Use last 123 tickframes of data: `--timerange=-123`
|
||||
- Use first 123 tickframes of data: `--timerange=123-`
|
||||
- Use tickframes from line 123 through 456: `--timerange=123-456`
|
||||
|
||||
|
||||
Incoming feature, not implemented yet:
|
||||
- `--timerange=-20180131`
|
||||
- `--timerange=20180101-`
|
||||
- `--timerange=20180101-20181231`
|
||||
|
||||
|
||||
**Update testdata directory**
|
||||
To update your testdata directory, or download into another testdata directory:
|
||||
```bash
|
||||
mkdir freqtrade/tests/testdata-20180113
|
||||
cp freqtrade/tests/testdata/pairs.json freqtrade/tests/testdata-20180113
|
||||
cd freqtrade/tests/testdata-20180113
|
||||
mkdir -p user_data/data/testdata-20180113
|
||||
cp freqtrade/tests/testdata/pairs.json user_data/data-20180113
|
||||
cd user_data/data-20180113
|
||||
```
|
||||
|
||||
Possibly edit pairs.json file to include/exclude pairs
|
||||
|
||||
python download_backtest_data.py -p pairs.json
|
||||
```bash
|
||||
python freqtrade/tests/testdata/download_backtest_data.py -p pairs.json
|
||||
```
|
||||
|
||||
The script will read your pairs.json file, and download ticker data
|
||||
|
@ -3,21 +3,55 @@ This page explains where to customize your strategies, and add new
|
||||
indicators.
|
||||
|
||||
## Table of Contents
|
||||
- [Change your strategy](#change-your-strategy)
|
||||
- [Install a custom strategy file](#install-a-custom-strategy-file)
|
||||
- [Customize your strategy](#change-your-strategy)
|
||||
- [Add more Indicator](#add-more-indicator)
|
||||
- [Where is the default strategy](#where-is-the-default-strategy)
|
||||
|
||||
Since the version `0.16.0` the bot allows using custom strategy file.
|
||||
|
||||
## Install a custom strategy file
|
||||
This is very simple. Copy paste your strategy file into the folder
|
||||
`user_data/strategies`.
|
||||
|
||||
Let assume you have a strategy file `awesome-strategy.py`:
|
||||
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py`
|
||||
2. Start the bot with the param `--strategy awesome-strategy` (the parameter is the name of the file without '.py')
|
||||
|
||||
```bash
|
||||
python3 ./freqtrade/main.py --strategy awesome_strategy
|
||||
```
|
||||
|
||||
## Change your strategy
|
||||
The bot is using buy and sell strategies to buy and sell your trades.
|
||||
Both are customizable.
|
||||
The bot includes a default strategy file. However, we recommend you to
|
||||
use your own file to not have to lose your parameters everytime the default
|
||||
strategy file will be updated on Github. Put your custom strategy file
|
||||
into the folder `user_data/strategies`.
|
||||
|
||||
### Buy strategy
|
||||
The default buy strategy is located in the file
|
||||
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73-L92).
|
||||
Edit the function `populate_buy_trend()` to update your buy strategy.
|
||||
A strategy file contains all the information needed to build a good strategy:
|
||||
- Buy strategy rules
|
||||
- Sell strategy rules
|
||||
- Minimal ROI recommended
|
||||
- Stoploss recommended
|
||||
- Hyperopt parameter
|
||||
|
||||
Sample:
|
||||
The bot also include a sample strategy you can update: `user_data/strategies/test_strategy.py`.
|
||||
You can test it with the parameter: `--strategy test_strategy`
|
||||
|
||||
```bash
|
||||
python3 ./freqtrade/main.py --strategy awesome_strategy
|
||||
```
|
||||
|
||||
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||
file as reference.**
|
||||
|
||||
### Buy strategy
|
||||
Edit the method `populate_buy_trend()` into your strategy file to
|
||||
update your buy strategy.
|
||||
|
||||
Sample from `user_data/strategies/test_strategy.py`:
|
||||
```python
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
@ -25,14 +59,9 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['rsi'] < 35) &
|
||||
(dataframe['fastd'] < 35) &
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['plus_di'] > 0.5)
|
||||
) |
|
||||
(
|
||||
(dataframe['adx'] > 65) &
|
||||
(dataframe['plus_di'] > 0.5)
|
||||
(dataframe['tema'] <= dataframe['blower']) &
|
||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||
),
|
||||
'buy'] = 1
|
||||
|
||||
@ -40,41 +69,31 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
```
|
||||
|
||||
### Sell strategy
|
||||
The default buy strategy is located in the file
|
||||
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L95-L115)
|
||||
Edit the function `populate_sell_trend()` to update your buy strategy.
|
||||
Edit the method `populate_sell_trend()` into your strategy file to
|
||||
update your sell strategy.
|
||||
|
||||
Sample:
|
||||
Sample from `user_data/strategies/test_strategy.py`:
|
||||
```python
|
||||
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(
|
||||
(crossed_above(dataframe['rsi'], 70)) |
|
||||
(crossed_above(dataframe['fastd'], 70))
|
||||
) &
|
||||
(dataframe['adx'] > 10) &
|
||||
(dataframe['minus_di'] > 0)
|
||||
) |
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['minus_di'] > 0.5)
|
||||
(dataframe['tema'] > dataframe['blower']) &
|
||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
||||
```
|
||||
|
||||
## Add more Indicator
|
||||
As you have seen, buy and sell strategies need indicators. You can see
|
||||
the indicators in the file
|
||||
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L95-L115).
|
||||
Of course you can add more indicators by extending the list contained in
|
||||
the function `populate_indicators()`.
|
||||
As you have seen, buy and sell strategies need indicators. You can add
|
||||
more indicators by extending the list contained in
|
||||
the method `populate_indicators()` from your strategy file.
|
||||
|
||||
Sample:
|
||||
```python
|
||||
@ -111,6 +130,15 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
return dataframe
|
||||
```
|
||||
|
||||
**Want more indicators example?**
|
||||
Look into the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
||||
Then uncomment indicators you need.
|
||||
|
||||
|
||||
### Where is the default strategy?
|
||||
The default buy strategy is located in the file
|
||||
[freqtrade/default_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
|
||||
|
||||
|
||||
## Next step
|
||||
Now you have a perfect strategy you probably want to backtesting it.
|
||||
|
@ -22,19 +22,21 @@ positional arguments:
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-c PATH, --config PATH
|
||||
specify configuration file (default: config.json)
|
||||
-v, --verbose be verbose
|
||||
--version show program's version number and exit
|
||||
-dd PATH, --datadir PATH
|
||||
Path is from where backtesting and hyperopt will load the
|
||||
ticker data files (default freqdata/tests/testdata).
|
||||
--dynamic-whitelist [INT]
|
||||
dynamically generate and update whitelist based on 24h
|
||||
BaseVolume (Default 20 currencies)
|
||||
-c PATH, --config PATH
|
||||
specify configuration file (default: config.json)
|
||||
-s PATH, --strategy PATH
|
||||
specify strategy file (default:
|
||||
freqtrade/strategy/default_strategy.py)
|
||||
--dry-run-db Force dry run to use a local DB
|
||||
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
||||
only if dry_run is enabled.
|
||||
-dd PATH, --datadir PATH
|
||||
path to backtest data (default freqdata/tests/testdata
|
||||
--dynamic-whitelist [INT]
|
||||
dynamically generate and update whitelist based on 24h
|
||||
BaseVolume (Default 20 currencies)
|
||||
```
|
||||
|
||||
### How to use a different config file?
|
||||
@ -45,6 +47,33 @@ default, the bot will load the file `./config.json`
|
||||
python3 ./freqtrade/main.py -c path/far/far/away/config.json
|
||||
```
|
||||
|
||||
### How to use --strategy?
|
||||
This parameter will allow you to load your custom strategy file. Per
|
||||
default without `--strategy` or `-s` the bol will load the
|
||||
`default_strategy` included with the bot (`freqtrade/strategy/default_strategy.py`).
|
||||
|
||||
The bot will search your strategy file into `user_data/strategies` and
|
||||
`freqtrade/strategy`.
|
||||
|
||||
To load a strategy, simply pass the file name (without .py) in this
|
||||
parameters.
|
||||
|
||||
**Example:**
|
||||
In `user_data/strategies` you have a file `my_awesome_strategy.py` to
|
||||
load it:
|
||||
```bash
|
||||
python3 ./freqtrade/main.py --strategy my_awesome_strategy
|
||||
```
|
||||
|
||||
If the bot does not find your strategy file, it will fallback to the
|
||||
`default_strategy`.
|
||||
|
||||
Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||
|
||||
#### How to install a strategy?
|
||||
This is very simple. Copy paste your strategy file into the folder
|
||||
`user_data/strategies`. And voila, the bot is ready to use it.
|
||||
|
||||
### How to use --dynamic-whitelist?
|
||||
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
|
||||
on BaseVolume. This value can be changed when you run the script.
|
||||
|
@ -17,10 +17,11 @@ The table below will list all configuration parameters.
|
||||
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have.
|
||||
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
|
||||
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged.
|
||||
| `ticker_interval` | [1, 5, 30, 60, 1440] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Defaut is 5 minutes
|
||||
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
|
||||
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
||||
| `minimal_roi` | See below | Yes | Set the threshold in percent the bot will use to sell a trade. More information below.
|
||||
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below.
|
||||
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
||||
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
||||
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
|
||||
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
||||
| `exchange.name` | bittrex | Yes | Name of the exchange class to use.
|
||||
@ -29,9 +30,10 @@ The table below will list all configuration parameters.
|
||||
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
||||
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
||||
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
||||
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
|
||||
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
||||
| `telegram.token` | token | No | Your Telegram bot token. Only required is `enable` is `true`.
|
||||
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required is `enable` is `true`.
|
||||
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
||||
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
||||
| `initial_state` | running | No | Defines the initial application state. More information below.
|
||||
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
||||
|
||||
@ -51,11 +53,19 @@ See the example below:
|
||||
},
|
||||
```
|
||||
|
||||
Most of the strategy files already include the optimal `minimal_roi`
|
||||
value. This parameter is optional. If you use it, it will take over the
|
||||
`minimal_roi` value from the strategy file.
|
||||
|
||||
### Understand stoploss
|
||||
`stoploss` is loss in percentage that should trigger a sale.
|
||||
For example value `-0.10` will cause immediate sell if the
|
||||
profit dips below -10% for a given trade. This parameter is optional.
|
||||
|
||||
Most of the strategy files already include the optimal `stoploss`
|
||||
value. This parameter is optional. If you use it, it will take over the
|
||||
`stoploss` value from the strategy file.
|
||||
|
||||
### Understand initial_state
|
||||
`initial_state` is an optional field that defines the initial application state.
|
||||
Possible values are `running` or `stopped`. (default=`running`)
|
||||
|
@ -14,14 +14,13 @@ parameters with Hyperopt.
|
||||
|
||||
## Prepare Hyperopt
|
||||
Before we start digging in Hyperopt, we recommend you to take a look at
|
||||
out Hyperopt file
|
||||
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
|
||||
your strategy file located into [user_data/strategies/](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||
|
||||
### 1. Configure your Guards and Triggers
|
||||
There are two places you need to change to add a new buy strategy for
|
||||
testing:
|
||||
- Inside the [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L167-L207).
|
||||
- Inside the [SPACE dict](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L47-L94).
|
||||
There are two places you need to change in your strategy file to add a
|
||||
new buy strategy for testing:
|
||||
- Inside [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294).
|
||||
- Inside [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
|
||||
|
||||
There you have two different type of indicators: 1. `guards` and 2.
|
||||
`triggers`.
|
||||
@ -38,10 +37,10 @@ ADX > 10*".
|
||||
|
||||
|
||||
If you have updated the buy strategy, means change the content of
|
||||
`populate_buy_trend()` function you have to update the `guards` and
|
||||
`populate_buy_trend()` method you have to update the `guards` and
|
||||
`triggers` hyperopts must used.
|
||||
|
||||
As for an example if your `populate_buy_trend()` function is:
|
||||
As for an example if your `populate_buy_trend()` method is:
|
||||
```python
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
dataframe.loc[
|
||||
@ -56,10 +55,10 @@ Your hyperopt file must contains `guards` to find the right value for
|
||||
`(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That
|
||||
means you will need to enable/disable triggers.
|
||||
|
||||
In our case the `SPACE` and `populate_buy_trend` in hyperopt.py file
|
||||
In our case the `SPACE` and `populate_buy_trend` in your strategy file
|
||||
will be look like:
|
||||
```python
|
||||
SPACE = {
|
||||
space = {
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
||||
@ -82,7 +81,7 @@ SPACE = {
|
||||
|
||||
...
|
||||
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if params['adx']['enabled']:
|
||||
@ -111,13 +110,13 @@ cannot use your config file. It is also made on purpose to allow you
|
||||
testing your strategy with different configurations.
|
||||
|
||||
The Hyperopt configuration is located in
|
||||
[freqtrade/optimize/hyperopt_conf.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt_conf.py).
|
||||
[user_data/hyperopt_conf.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/hyperopt_conf.py).
|
||||
|
||||
|
||||
## Advanced notions
|
||||
### Understand the Guards and Triggers
|
||||
When you need to add the new guards and triggers to be hyperopt
|
||||
parameters, you do this by adding them into the [SPACE dict](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L47-L94).
|
||||
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
|
||||
|
||||
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
|
||||
|
||||
@ -149,9 +148,8 @@ for best working algo.
|
||||
|
||||
### Add a new Indicators
|
||||
If you want to test an indicator that isn't used by the bot currently,
|
||||
you need to add it to
|
||||
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L40-L70)
|
||||
inside the `populate_indicators` function.
|
||||
you need to add it to your strategy file (example: [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py))
|
||||
inside the `populate_indicators()` method.
|
||||
|
||||
## Execute Hyperopt
|
||||
Once you have updated your hyperopt configuration you can run it.
|
||||
@ -165,8 +163,18 @@ python3 ./freqtrade/main.py -c config.json hyperopt
|
||||
|
||||
### Execute hyperopt with different ticker-data source
|
||||
If you would like to learn parameters using an alternate ticke-data that
|
||||
you have on-disk, use the --datadir PATH option. Default hyperopt will
|
||||
use data from directory freqtrade/tests/testdata.
|
||||
you have on-disk, use the `--datadir PATH` option. Default hyperopt will
|
||||
use data from directory `user_data/data`.
|
||||
|
||||
### Running hyperopt with smaller testset
|
||||
|
||||
Use the --timeperiod argument to change how much of the testset
|
||||
you want to use. The last N ticks/timeframes will be used.
|
||||
Example:
|
||||
|
||||
```bash
|
||||
python3 ./freqtrade/main.py hyperopt --timeperiod -200
|
||||
```
|
||||
|
||||
### Hyperopt with MongoDB
|
||||
Hyperopt with MongoDB, is like Hyperopt under steroids. As you saw by
|
||||
@ -260,15 +268,11 @@ customizable value.
|
||||
- and so on...
|
||||
|
||||
|
||||
You have to look from
|
||||
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L170-L200)
|
||||
what those values match to.
|
||||
You have to look inside your strategy file into `buy_strategy_generator()`
|
||||
method, what those values match to.
|
||||
|
||||
So for example you had `adx:` with the `value: 15.0` so we would look
|
||||
at `adx`-block from
|
||||
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L178-L179).
|
||||
That translates to the following code block to
|
||||
[analyze.populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73)
|
||||
at `adx`-block, that translates to the following code block:
|
||||
```
|
||||
(dataframe['adx'] > 15.0)
|
||||
```
|
||||
@ -276,7 +280,7 @@ That translates to the following code block to
|
||||
So translating your whole hyperopt result to as the new buy-signal
|
||||
would be the following:
|
||||
```
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 15.0) & # adx-value
|
||||
|
48
docs/plotting.md
Normal file
48
docs/plotting.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Plotting
|
||||
This page explains how to plot prices, indicator, profits.
|
||||
|
||||
## Table of Contents
|
||||
- [Plot price and indicators](#plot-price-and-indicators)
|
||||
- [Plot profit](#plot-profit)
|
||||
|
||||
## Plot price and indicators
|
||||
Usage for the price plotter:
|
||||
script/plot_dataframe.py [-h] [-p pair]
|
||||
|
||||
Example
|
||||
```
|
||||
python script/plot_dataframe.py -p BTC_ETH,BTC_LTC
|
||||
```
|
||||
|
||||
The -p pair argument, can be used to specify what
|
||||
pair you would like to plot.
|
||||
|
||||
|
||||
## Plot profit
|
||||
|
||||
The profit plotter show 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
|
||||
more of an estimate.
|
||||
3) Each pair individually profit
|
||||
|
||||
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.
|
||||
Perhaps you want an algorithm that steadily makes small profits,
|
||||
or one that acts less seldom, but makes big swings.
|
||||
|
||||
The third graph can be useful to spot outliers, events in pairs
|
||||
that makes profit spikes.
|
||||
|
||||
Usage for the profit plotter:
|
||||
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
|
||||
```
|
||||
python python scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p BTC_LTC
|
||||
```
|
@ -1,5 +1,5 @@
|
||||
""" FreqTrade bot """
|
||||
__version__ = '0.15.1'
|
||||
__version__ = '0.16.0'
|
||||
|
||||
|
||||
class DependencyException(BaseException):
|
||||
|
@ -7,11 +7,10 @@ from enum import Enum
|
||||
from typing import Dict, List
|
||||
|
||||
import arrow
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.exchange import get_ticker_history
|
||||
from freqtrade.strategy.strategy import Strategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -30,8 +29,9 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||
"""
|
||||
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
|
||||
frame = DataFrame(ticker) \
|
||||
.drop('BV', 1) \
|
||||
.rename(columns=columns)
|
||||
if 'BV' in frame:
|
||||
frame.drop('BV', 1, inplace=True)
|
||||
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
|
||||
frame.sort_values('date', inplace=True)
|
||||
return frame
|
||||
@ -45,181 +45,8 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
# ------------------------------------
|
||||
|
||||
# ADX
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
|
||||
# Awesome oscillator
|
||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
"""
|
||||
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
"""
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
|
||||
# MFI
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
|
||||
# Minus Directional Indicator / Movement
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# Plus Directional Indicator / Movement
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
"""
|
||||
# ROC
|
||||
dataframe['roc'] = ta.ROC(dataframe)
|
||||
"""
|
||||
# RSI
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
"""
|
||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||
|
||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
|
||||
# Stoch
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
"""
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
"""
|
||||
# Stoch RSI
|
||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
"""
|
||||
|
||||
# Overlap Studies
|
||||
# ------------------------------------
|
||||
|
||||
# Previous Bollinger bands
|
||||
# Because ta.BBANDS implementation is broken with small numbers, it actually
|
||||
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
|
||||
# and use middle band instead.
|
||||
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
||||
"""
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
"""
|
||||
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
|
||||
# SAR Parabol
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
|
||||
# SMA - Simple Moving Average
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
# Cycle Indicator
|
||||
# ------------------------------------
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hammer: values [0, 100]
|
||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
|
||||
# Inverted Hammer: values [0, 100]
|
||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
|
||||
# Dragonfly Doji: values [0, 100]
|
||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
|
||||
# Piercing Line: values [0, 100]
|
||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
|
||||
# Morningstar: values [0, 100]
|
||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
|
||||
# Three White Soldiers: values [0, 100]
|
||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hanging Man: values [0, 100]
|
||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
|
||||
# Shooting Star: values [0, 100]
|
||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
|
||||
# Gravestone Doji: values [0, 100]
|
||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
|
||||
# Dark Cloud Cover: values [0, 100]
|
||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
|
||||
# Evening Doji Star: values [0, 100]
|
||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
|
||||
# Evening Star: values [0, 100]
|
||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Three Line Strike: values [0, -100, 100]
|
||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
|
||||
# Spinning Top: values [0, -100, 100]
|
||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
|
||||
# Engulfing: values [0, -100, 100]
|
||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
|
||||
# Harami: values [0, -100, 100]
|
||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
|
||||
# Three Outside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
|
||||
# Three Inside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
"""
|
||||
|
||||
# Chart type
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Heikinashi stategy
|
||||
heikinashi = qtpylib.heikinashi(dataframe)
|
||||
dataframe['ha_open'] = heikinashi['open']
|
||||
dataframe['ha_close'] = heikinashi['close']
|
||||
dataframe['ha_high'] = heikinashi['high']
|
||||
dataframe['ha_low'] = heikinashi['low']
|
||||
"""
|
||||
|
||||
return dataframe
|
||||
strategy = Strategy()
|
||||
return strategy.populate_indicators(dataframe=dataframe)
|
||||
|
||||
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
@ -228,20 +55,8 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['rsi'] < 35) &
|
||||
(dataframe['fastd'] < 35) &
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['plus_di'] > 0.5)
|
||||
) |
|
||||
(
|
||||
(dataframe['adx'] > 65) &
|
||||
(dataframe['plus_di'] > 0.5)
|
||||
),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
strategy = Strategy()
|
||||
return strategy.populate_buy_trend(dataframe=dataframe)
|
||||
|
||||
|
||||
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
||||
@ -250,21 +65,8 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(
|
||||
(qtpylib.crossed_above(dataframe['rsi'], 70)) |
|
||||
(qtpylib.crossed_above(dataframe['fastd'], 70))
|
||||
) &
|
||||
(dataframe['adx'] > 10) &
|
||||
(dataframe['minus_di'] > 0)
|
||||
) |
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['minus_di'] > 0.5)
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
||||
strategy = Strategy()
|
||||
return strategy.populate_sell_trend(dataframe=dataframe)
|
||||
|
||||
|
||||
def analyze_ticker(ticker_history: List[Dict]) -> DataFrame:
|
||||
@ -280,36 +82,40 @@ def analyze_ticker(ticker_history: List[Dict]) -> DataFrame:
|
||||
return dataframe
|
||||
|
||||
|
||||
def get_signal(pair: str, signal: SignalType) -> bool:
|
||||
# FIX: Maybe return False, if an error has occured,
|
||||
# Otherwise we might mask an error as an non-signal-scenario
|
||||
def get_signal(pair: str, interval: int) -> (bool, bool):
|
||||
"""
|
||||
Calculates current signal based several technical analysis indicators
|
||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
||||
:return: True if pair is good for buying, False otherwise
|
||||
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||
"""
|
||||
ticker_hist = get_ticker_history(pair)
|
||||
ticker_hist = get_ticker_history(pair, interval)
|
||||
if not ticker_hist:
|
||||
logger.warning('Empty ticker history for pair %s', pair)
|
||||
return False
|
||||
return (False, False) # return False ?
|
||||
|
||||
try:
|
||||
dataframe = analyze_ticker(ticker_hist)
|
||||
except ValueError as ex:
|
||||
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
|
||||
return False
|
||||
return (False, False) # return False ?
|
||||
except Exception as ex:
|
||||
logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex))
|
||||
return False
|
||||
return (False, False) # return False ?
|
||||
|
||||
if dataframe.empty:
|
||||
return False
|
||||
logger.warning('Empty dataframe for pair %s', pair)
|
||||
return (False, False) # return False ?
|
||||
|
||||
latest = dataframe.iloc[-1]
|
||||
|
||||
# Check if dataframe is out of date
|
||||
signal_date = arrow.get(latest['date'])
|
||||
if signal_date < arrow.now() - timedelta(minutes=10):
|
||||
return False
|
||||
logger.warning('Too old dataframe for pair %s', pair)
|
||||
return (False, False) # return False ?
|
||||
|
||||
result = latest[signal.value] == 1
|
||||
logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
|
||||
return result
|
||||
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
||||
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell))
|
||||
return (buy, sell)
|
||||
|
@ -139,7 +139,7 @@ def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
|
||||
|
||||
|
||||
@cached(TTLCache(maxsize=100, ttl=30))
|
||||
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List[Dict]:
|
||||
def get_ticker_history(pair: str, tick_interval) -> List[Dict]:
|
||||
return _API.get_ticker_history(pair, tick_interval)
|
||||
|
||||
|
||||
|
@ -122,9 +122,10 @@ class Bittrex(Exchange):
|
||||
raise OperationalException('{message} params=({pair})'.format(
|
||||
message=data['message'],
|
||||
pair=pair))
|
||||
|
||||
keys = ['Bid', 'Ask', 'Last']
|
||||
if not data.get('result') or\
|
||||
not all(key in data.get('result', {}) for key in ['Bid', 'Ask', 'Last']):
|
||||
not all(key in data.get('result', {}) for key in keys) or\
|
||||
not all(data.get('result', {})[key] is not None for key in keys):
|
||||
raise ContentDecodingError('{message} params=({pair})'.format(
|
||||
message='Got invalid response from bittrex',
|
||||
pair=pair))
|
||||
@ -141,6 +142,12 @@ class Bittrex(Exchange):
|
||||
interval = 'oneMin'
|
||||
elif tick_interval == 5:
|
||||
interval = 'fiveMin'
|
||||
elif tick_interval == 30:
|
||||
interval = 'thirtyMin'
|
||||
elif tick_interval == 60:
|
||||
interval = 'hour'
|
||||
elif tick_interval == 1440:
|
||||
interval = 'Day'
|
||||
else:
|
||||
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
|
||||
|
||||
|
@ -48,7 +48,10 @@ class CryptoFiat():
|
||||
return self._expiration - time.time() <= 0
|
||||
|
||||
|
||||
class CryptoToFiatConverter():
|
||||
class CryptoToFiatConverter(object):
|
||||
__instance = None
|
||||
_coinmarketcap = None
|
||||
|
||||
# Constants
|
||||
SUPPORTED_FIAT = [
|
||||
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
||||
@ -57,12 +60,16 @@ class CryptoToFiatConverter():
|
||||
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
|
||||
]
|
||||
|
||||
def __init__(self) -> None:
|
||||
try:
|
||||
self._coinmarketcap = Pymarketcap()
|
||||
except BaseException:
|
||||
self._coinmarketcap = None
|
||||
def __new__(cls):
|
||||
if CryptoToFiatConverter.__instance is None:
|
||||
CryptoToFiatConverter.__instance = object.__new__(cls)
|
||||
try:
|
||||
CryptoToFiatConverter._coinmarketcap = Pymarketcap()
|
||||
except BaseException:
|
||||
CryptoToFiatConverter._coinmarketcap = None
|
||||
return CryptoToFiatConverter.__instance
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._pairs = []
|
||||
|
||||
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||
|
@ -14,11 +14,12 @@ from cachetools import cached, TTLCache
|
||||
|
||||
from freqtrade import (DependencyException, OperationalException, __version__,
|
||||
exchange, persistence, rpc)
|
||||
from freqtrade.analyze import SignalType, get_signal
|
||||
from freqtrade.analyze import get_signal
|
||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.misc import (State, get_state, load_config, parse_args,
|
||||
throttle, update_state)
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy.strategy import Strategy
|
||||
|
||||
logger = logging.getLogger('freqtrade')
|
||||
|
||||
@ -54,12 +55,49 @@ def refresh_whitelist(whitelist: List[str]) -> List[str]:
|
||||
return final_list
|
||||
|
||||
|
||||
def _process(nb_assets: Optional[int] = 0) -> bool:
|
||||
def process_maybe_execute_buy(conf, interval):
|
||||
"""
|
||||
Tries to execute a buy trade in a safe way
|
||||
:return: True if executed
|
||||
"""
|
||||
try:
|
||||
# Create entity and execute trade
|
||||
if create_trade(float(_CONF['stake_amount']), interval):
|
||||
return True
|
||||
else:
|
||||
logger.info(
|
||||
'Checked all whitelisted currencies. '
|
||||
'Found no suitable entry positions for buying. Will keep looking ...'
|
||||
)
|
||||
return False
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to create trade: %s', exception)
|
||||
return False
|
||||
|
||||
|
||||
def process_maybe_execute_sell(trade, interval):
|
||||
"""
|
||||
Tries to execute a sell trade
|
||||
:return: True if executed
|
||||
"""
|
||||
# Get order details for actual price per unit
|
||||
if trade.open_order_id:
|
||||
# Update trade with order values
|
||||
logger.info('Got open order for %s', trade)
|
||||
trade.update(exchange.get_order(trade.open_order_id))
|
||||
|
||||
if trade.is_open and trade.open_order_id is None:
|
||||
# Check if we can sell our current pair
|
||||
return handle_trade(trade, interval)
|
||||
return False
|
||||
|
||||
|
||||
def _process(interval: int, nb_assets: Optional[int] = 0) -> bool:
|
||||
"""
|
||||
Queries the persistence layer for open trades and handles them,
|
||||
otherwise a new trade is created.
|
||||
:param: nb_assets: the maximum number of pairs to be traded at the same time
|
||||
:return: True if a trade has been created or closed, False otherwise
|
||||
:return: True if one or more trades has been created or closed, False otherwise
|
||||
"""
|
||||
state_changed = False
|
||||
try:
|
||||
@ -77,33 +115,16 @@ def _process(nb_assets: Optional[int] = 0) -> bool:
|
||||
# Query trades from persistence layer
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
if len(trades) < _CONF['max_open_trades']:
|
||||
try:
|
||||
# Create entity and execute trade
|
||||
state_changed = create_trade(float(_CONF['stake_amount']))
|
||||
if not state_changed:
|
||||
logger.info(
|
||||
'Checked all whitelisted currencies. '
|
||||
'Found no suitable entry positions for buying. Will keep looking ...'
|
||||
)
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to create trade: %s', exception)
|
||||
state_changed = process_maybe_execute_buy(_CONF, interval)
|
||||
|
||||
for trade in trades:
|
||||
# Get order details for actual price per unit
|
||||
if trade.open_order_id:
|
||||
# Update trade with order values
|
||||
logger.info('Got open order for %s', trade)
|
||||
trade.update(exchange.get_order(trade.open_order_id))
|
||||
|
||||
if trade.is_open and trade.open_order_id is None:
|
||||
# Check if we can sell our current pair
|
||||
state_changed = handle_trade(trade) or state_changed
|
||||
state_changed |= process_maybe_execute_sell(trade, interval)
|
||||
|
||||
if 'unfilledtimeout' in _CONF:
|
||||
# Check and handle any timed out open orders
|
||||
check_handle_timedout(_CONF['unfilledtimeout'])
|
||||
|
||||
Trade.session.flush()
|
||||
|
||||
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
||||
logger.warning(
|
||||
'Got %s in _process(), retrying in 30 seconds...',
|
||||
@ -120,6 +141,59 @@ def _process(nb_assets: Optional[int] = 0) -> bool:
|
||||
return state_changed
|
||||
|
||||
|
||||
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
||||
# it is conditionally called in the
|
||||
# handle_timedout_limit_sell()?
|
||||
def handle_timedout_limit_buy(trade: Trade, order: Dict) -> bool:
|
||||
"""Buy timeout - cancel order
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
exchange.cancel_order(trade.open_order_id)
|
||||
if order['remaining'] == order['amount']:
|
||||
# if trade is not partially completed, just delete the trade
|
||||
Trade.session.delete(trade)
|
||||
# FIX? do we really need to flush, caller of
|
||||
# check_handle_timedout will flush afterwards
|
||||
Trade.session.flush()
|
||||
logger.info('Buy order timeout for %s.', trade)
|
||||
rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
|
||||
trade.pair.replace('_', '/')))
|
||||
return True
|
||||
else:
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
# and close the order
|
||||
trade.amount = order['amount'] - order['remaining']
|
||||
trade.stake_amount = trade.amount * trade.open_rate
|
||||
trade.open_order_id = None
|
||||
logger.info('Partial buy order timeout for %s.', trade)
|
||||
rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
|
||||
trade.pair.replace('_', '/')))
|
||||
return False
|
||||
|
||||
|
||||
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
||||
def handle_timedout_limit_sell(trade: Trade, order: Dict) -> bool:
|
||||
"""
|
||||
Sell timeout - cancel order and update trade
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
if order['remaining'] == order['amount']:
|
||||
# if trade is not partially completed, just cancel the trade
|
||||
exchange.cancel_order(trade.open_order_id)
|
||||
trade.close_rate = None
|
||||
trade.close_profit = None
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
|
||||
trade.pair.replace('_', '/')))
|
||||
logger.info('Sell order timeout for %s.', trade)
|
||||
return True
|
||||
else:
|
||||
# TODO: figure out how to handle partially complete sell orders
|
||||
return False
|
||||
|
||||
|
||||
def check_handle_timedout(timeoutvalue: int) -> None:
|
||||
"""
|
||||
Check if any orders are timed out and cancel if neccessary
|
||||
@ -129,39 +203,21 @@ def check_handle_timedout(timeoutvalue: int) -> None:
|
||||
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
|
||||
|
||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||
order = exchange.get_order(trade.open_order_id)
|
||||
try:
|
||||
order = exchange.get_order(trade.open_order_id)
|
||||
except requests.exceptions.RequestException:
|
||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||
continue
|
||||
ordertime = arrow.get(order['opened'])
|
||||
|
||||
# Check if trade is still actually open
|
||||
if int(order['remaining']) == 0:
|
||||
continue
|
||||
|
||||
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
|
||||
# Buy timeout - cancel order
|
||||
exchange.cancel_order(trade.open_order_id)
|
||||
if order['remaining'] == order['amount']:
|
||||
# if trade is not partially completed, just delete the trade
|
||||
Trade.session.delete(trade)
|
||||
Trade.session.flush()
|
||||
logger.info('Buy order timeout for %s.', trade)
|
||||
else:
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
# and close the order
|
||||
trade.amount = order['amount'] - order['remaining']
|
||||
trade.stake_amount = trade.amount * trade.open_rate
|
||||
trade.open_order_id = None
|
||||
logger.info('Partial buy order timeout for %s.', trade)
|
||||
handle_timedout_limit_buy(trade, order)
|
||||
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
|
||||
# Sell timeout - cancel order and update trade
|
||||
if order['remaining'] == order['amount']:
|
||||
# if trade is not partially completed, just cancel the trade
|
||||
exchange.cancel_order(trade.open_order_id)
|
||||
trade.close_rate = None
|
||||
trade.close_profit = None
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
logger.info('Sell order timeout for %s.', trade)
|
||||
return True
|
||||
else:
|
||||
# TODO: figure out how to handle partially complete sell orders
|
||||
pass
|
||||
handle_timedout_limit_sell(trade, order)
|
||||
|
||||
|
||||
def execute_sell(trade: Trade, limit: float) -> None:
|
||||
@ -177,12 +233,25 @@ def execute_sell(trade: Trade, limit: float) -> None:
|
||||
|
||||
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
|
||||
profit_trade = trade.calc_profit(rate=limit)
|
||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||
profit = trade.calc_profit_percent(current_rate)
|
||||
|
||||
message = '*{exchange}:* Selling [{pair}]({pair_url}) with limit `{limit:.8f}`'.format(
|
||||
message = """*{exchange}:* Selling
|
||||
*Current Pair:* [{pair}]({pair_url})
|
||||
*Limit:* `{limit}`
|
||||
*Amount:* `{amount}`
|
||||
*Open Rate:* `{open_rate:.8f}`
|
||||
*Current Rate:* `{current_rate:.8f}`
|
||||
*Profit:* `{profit:.2f}%`
|
||||
""".format(
|
||||
exchange=trade.exchange,
|
||||
pair=trade.pair.replace('_', '/'),
|
||||
pair=trade.pair,
|
||||
pair_url=exchange.get_pair_detail_url(trade.pair),
|
||||
limit=limit
|
||||
limit=limit,
|
||||
open_rate=trade.open_rate,
|
||||
current_rate=current_rate,
|
||||
amount=round(trade.amount, 8),
|
||||
profit=round(profit * 100, 2),
|
||||
)
|
||||
|
||||
# For regular case, when the configuration exists
|
||||
@ -221,14 +290,16 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -
|
||||
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
|
||||
:return True if bot should sell at current rate
|
||||
"""
|
||||
strategy = Strategy()
|
||||
|
||||
current_profit = trade.calc_profit_percent(current_rate)
|
||||
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
|
||||
if strategy.stoploss is not None and current_profit < float(strategy.stoploss):
|
||||
logger.debug('Stop loss hit.')
|
||||
return True
|
||||
|
||||
# Check if time matches and current rate is above threshold
|
||||
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
||||
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
||||
for duration, threshold in sorted(strategy.minimal_roi.items()):
|
||||
if time_diff > float(duration) and current_profit > threshold:
|
||||
return True
|
||||
|
||||
@ -236,7 +307,7 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -
|
||||
return False
|
||||
|
||||
|
||||
def handle_trade(trade: Trade) -> bool:
|
||||
def handle_trade(trade: Trade, interval: int) -> bool:
|
||||
"""
|
||||
Sells the current pair if the threshold is reached and updates the trade record.
|
||||
:return: True if trade has been sold, False otherwise
|
||||
@ -247,24 +318,27 @@ def handle_trade(trade: Trade) -> bool:
|
||||
logger.debug('Handling %s ...', trade)
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
|
||||
# Check if minimal roi has been reached
|
||||
if min_roi_reached(trade, current_rate, datetime.utcnow()):
|
||||
(buy, sell) = (False, False)
|
||||
|
||||
if _CONF.get('experimental', {}).get('use_sell_signal'):
|
||||
(buy, sell) = get_signal(trade.pair, interval)
|
||||
|
||||
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
||||
if not buy and min_roi_reached(trade, current_rate, datetime.utcnow()):
|
||||
logger.debug('Executing sell due to ROI ...')
|
||||
execute_sell(trade, current_rate)
|
||||
return True
|
||||
|
||||
# Experimental: Check if sell signal has been enabled and triggered
|
||||
if _CONF.get('experimental', {}).get('use_sell_signal'):
|
||||
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
|
||||
if _CONF.get('experimental', {}).get('sell_profit_only'):
|
||||
logger.debug('Checking if trade is profitable ...')
|
||||
if trade.calc_profit(rate=current_rate) <= 0:
|
||||
return False
|
||||
logger.debug('Checking sell_signal ...')
|
||||
if get_signal(trade.pair, SignalType.SELL):
|
||||
logger.debug('Executing sell due to sell signal ...')
|
||||
execute_sell(trade, current_rate)
|
||||
return True
|
||||
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
|
||||
if _CONF.get('experimental', {}).get('sell_profit_only', False):
|
||||
logger.debug('Checking if trade is profitable ...')
|
||||
if not buy and trade.calc_profit(rate=current_rate) <= 0:
|
||||
return False
|
||||
|
||||
if sell and not buy:
|
||||
logger.debug('Executing sell due to sell signal ...')
|
||||
execute_sell(trade, current_rate)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@ -277,7 +351,7 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
|
||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||
|
||||
|
||||
def create_trade(stake_amount: float) -> bool:
|
||||
def create_trade(stake_amount: float, interval: int) -> bool:
|
||||
"""
|
||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||
if one pair triggers the buy_signal a new trade record gets created
|
||||
@ -305,7 +379,8 @@ def create_trade(stake_amount: float) -> bool:
|
||||
|
||||
# Pick pair based on StochRSI buy signals
|
||||
for _pair in whitelist:
|
||||
if get_signal(_pair, SignalType.BUY):
|
||||
(buy, sell) = get_signal(_pair, interval)
|
||||
if buy and not sell:
|
||||
pair = _pair
|
||||
break
|
||||
else:
|
||||
@ -360,6 +435,9 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
persistence.init(config, db_url)
|
||||
exchange.init(config)
|
||||
|
||||
strategy = Strategy()
|
||||
strategy.init(config)
|
||||
|
||||
# Set initial application state
|
||||
initial_state = config.get('initial_state')
|
||||
if initial_state:
|
||||
@ -398,7 +476,7 @@ def cleanup() -> None:
|
||||
exit(0)
|
||||
|
||||
|
||||
def main(sysargv=sys.argv[1:]) -> None:
|
||||
def main(sysargv=sys.argv[1:]) -> int:
|
||||
"""
|
||||
Loads and validates the config and handles the main loop
|
||||
:return: None
|
||||
@ -410,7 +488,7 @@ def main(sysargv=sys.argv[1:]) -> None:
|
||||
# A subcommand has been issued
|
||||
if hasattr(args, 'func'):
|
||||
args.func(args)
|
||||
exit(0)
|
||||
return 0
|
||||
|
||||
# Initialize logger
|
||||
logging.basicConfig(
|
||||
@ -427,6 +505,9 @@ def main(sysargv=sys.argv[1:]) -> None:
|
||||
# Load and validate configuration
|
||||
_CONF = load_config(args.config)
|
||||
|
||||
# Add the strategy file to use
|
||||
_CONF.update({'strategy': args.strategy})
|
||||
|
||||
# Initialize all modules and start main loop
|
||||
if args.dynamic_whitelist:
|
||||
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
|
||||
@ -444,6 +525,7 @@ def main(sysargv=sys.argv[1:]) -> None:
|
||||
try:
|
||||
init(_CONF)
|
||||
old_state = None
|
||||
|
||||
while True:
|
||||
new_state = get_state()
|
||||
# Log state transition
|
||||
@ -458,6 +540,7 @@ def main(sysargv=sys.argv[1:]) -> None:
|
||||
_process,
|
||||
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
|
||||
nb_assets=args.dynamic_whitelist,
|
||||
interval=int(_CONF.get('ticker_interval', 5))
|
||||
)
|
||||
old_state = new_state
|
||||
except KeyboardInterrupt:
|
||||
@ -466,7 +549,8 @@ def main(sysargv=sys.argv[1:]) -> None:
|
||||
logger.exception('Got fatal exception!')
|
||||
finally:
|
||||
cleanup()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main(sys.argv[1:])
|
||||
|
@ -4,6 +4,7 @@ import json
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
from jsonschema import Draft4Validator, validate
|
||||
@ -115,6 +116,22 @@ def common_args_parser(description: str):
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--datadir',
|
||||
help='path to backtest data (default freqdata/tests/testdata)',
|
||||
dest='datadir',
|
||||
default=os.path.join('freqtrade', 'tests', 'testdata'),
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s', '--strategy',
|
||||
help='specify strategy file (default: freqtrade/strategy/default_strategy.py)',
|
||||
dest='strategy',
|
||||
default='.default_strategy',
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@ -131,14 +148,6 @@ def parse_args(args: List[str], description: str):
|
||||
action='store_true',
|
||||
dest='dry_run_db',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-dd', '--datadir',
|
||||
help='path to backtest data (default freqdata/tests/testdata',
|
||||
dest='datadir',
|
||||
default=os.path.join('freqtrade', 'tests', 'testdata'),
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dynamic-whitelist',
|
||||
help='dynamically generate and update whitelist \
|
||||
@ -154,6 +163,113 @@ def parse_args(args: List[str], description: str):
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
||||
def backtesting_options(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
'-l', '--live',
|
||||
action='store_true',
|
||||
dest='live',
|
||||
help='using live data',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-i', '--ticker-interval',
|
||||
help='specify ticker interval in minutes (1, 5, 30, 60, 1440)',
|
||||
dest='ticker_interval',
|
||||
default=5,
|
||||
type=int,
|
||||
metavar='INT',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--realistic-simulation',
|
||||
help='uses max_open_trades from config to simulate real world limitations',
|
||||
action='store_true',
|
||||
dest='realistic_simulation',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r', '--refresh-pairs-cached',
|
||||
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
|
||||
Use it if you want to run your backtesting with up-to-date data.',
|
||||
action='store_true',
|
||||
dest='refresh_pairs',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--export',
|
||||
help='Export backtest results, argument are: trades\
|
||||
Example --export=trades',
|
||||
type=str,
|
||||
default=None,
|
||||
dest='export',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timerange',
|
||||
help='Specify what timerange of data to use.',
|
||||
default=None,
|
||||
type=str,
|
||||
dest='timerange',
|
||||
)
|
||||
|
||||
|
||||
def hyperopt_options(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
'-e', '--epochs',
|
||||
help='specify number of epochs (default: 100)',
|
||||
dest='epochs',
|
||||
default=100,
|
||||
type=int,
|
||||
metavar='INT',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--use-mongodb',
|
||||
help='parallelize evaluations with mongodb (requires mongod in PATH)',
|
||||
dest='mongodb',
|
||||
action='store_true',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-i', '--ticker-interval',
|
||||
help='specify ticker interval in minutes (default: 5)',
|
||||
dest='ticker_interval',
|
||||
default=5,
|
||||
type=int,
|
||||
metavar='INT',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timerange',
|
||||
help='Specify what timerange of data to use.',
|
||||
default=None,
|
||||
type=str,
|
||||
dest='timerange',
|
||||
)
|
||||
|
||||
|
||||
def parse_timerange(text):
|
||||
if text is None:
|
||||
return None
|
||||
syntax = [('^-(\d{8})$', (None, 'date')),
|
||||
('^(\d{8})-$', ('date', None)),
|
||||
('^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||
('^(-\d+)$', (None, 'line')),
|
||||
('^(\d+)-$', ('line', None)),
|
||||
('^(\d+)-(\d+)$', ('index', 'index'))]
|
||||
for rex, stype in syntax:
|
||||
# Apply the regular expression to text
|
||||
m = re.match(rex, text)
|
||||
if m: # Regex has matched
|
||||
rvals = m.groups()
|
||||
n = 0
|
||||
start = None
|
||||
stop = None
|
||||
if stype[0]:
|
||||
start = rvals[n]
|
||||
if stype[0] != 'date':
|
||||
start = int(start)
|
||||
n += 1
|
||||
if stype[1]:
|
||||
stop = rvals[n]
|
||||
if stype[1] != 'date':
|
||||
stop = int(stop)
|
||||
return (stype, start, stop)
|
||||
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
||||
|
||||
|
||||
def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
||||
""" Builds and attaches all subcommands """
|
||||
from freqtrade.optimize import backtesting, hyperopt
|
||||
@ -163,59 +279,12 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
||||
# Add backtesting subcommand
|
||||
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
|
||||
backtesting_cmd.set_defaults(func=backtesting.start)
|
||||
backtesting_cmd.add_argument(
|
||||
'-l', '--live',
|
||||
action='store_true',
|
||||
dest='live',
|
||||
help='using live data',
|
||||
)
|
||||
backtesting_cmd.add_argument(
|
||||
'-i', '--ticker-interval',
|
||||
help='specify ticker interval in minutes (default: 5)',
|
||||
dest='ticker_interval',
|
||||
default=5,
|
||||
type=int,
|
||||
metavar='INT',
|
||||
)
|
||||
backtesting_cmd.add_argument(
|
||||
'--realistic-simulation',
|
||||
help='uses max_open_trades from config to simulate real world limitations',
|
||||
action='store_true',
|
||||
dest='realistic_simulation',
|
||||
)
|
||||
backtesting_cmd.add_argument(
|
||||
'-r', '--refresh-pairs-cached',
|
||||
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
|
||||
Use it if you want to run your backtesting with up-to-date data.',
|
||||
action='store_true',
|
||||
dest='refresh_pairs',
|
||||
)
|
||||
backtesting_options(backtesting_cmd)
|
||||
|
||||
# Add hyperopt subcommand
|
||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
||||
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
||||
hyperopt_cmd.add_argument(
|
||||
'-e', '--epochs',
|
||||
help='specify number of epochs (default: 100)',
|
||||
dest='epochs',
|
||||
default=100,
|
||||
type=int,
|
||||
metavar='INT',
|
||||
)
|
||||
hyperopt_cmd.add_argument(
|
||||
'--use-mongodb',
|
||||
help='parallelize evaluations with mongodb (requires mongod in PATH)',
|
||||
dest='mongodb',
|
||||
action='store_true',
|
||||
)
|
||||
hyperopt_cmd.add_argument(
|
||||
'-i', '--ticker-interval',
|
||||
help='specify ticker interval in minutes (default: 5)',
|
||||
dest='ticker_interval',
|
||||
default=5,
|
||||
type=int,
|
||||
metavar='INT',
|
||||
)
|
||||
hyperopt_options(hyperopt_cmd)
|
||||
|
||||
|
||||
# Required json-schema for user specified config
|
||||
@ -223,6 +292,7 @@ CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
||||
'ticker_interval': {'type': 'integer', 'enum': [1, 5, 30, 60, 1440]},
|
||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
||||
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
||||
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
|
||||
@ -276,7 +346,8 @@ CONF_SCHEMA = {
|
||||
'internals': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'process_throttle_secs': {'type': 'number'}
|
||||
'process_throttle_secs': {'type': 'number'},
|
||||
'interval': {'type': 'integer'}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -316,7 +387,6 @@ CONF_SCHEMA = {
|
||||
'stake_amount',
|
||||
'fiat_display_currency',
|
||||
'dry_run',
|
||||
'minimal_roi',
|
||||
'bid_strategy',
|
||||
'telegram'
|
||||
]
|
||||
|
@ -6,13 +6,28 @@ import os
|
||||
from typing import Optional, List, Dict
|
||||
from pandas import DataFrame
|
||||
from freqtrade.exchange import get_ticker_history
|
||||
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
|
||||
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
|
||||
|
||||
from freqtrade import misc
|
||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_tickerdata_file(datadir, pair, ticker_interval):
|
||||
def trim_tickerlist(tickerlist, timerange):
|
||||
(stype, start, stop) = timerange
|
||||
if stype == (None, 'line'):
|
||||
return tickerlist[stop:]
|
||||
elif stype == ('line', None):
|
||||
return tickerlist[0:start]
|
||||
elif stype == ('index', 'index'):
|
||||
return tickerlist[start:stop]
|
||||
else:
|
||||
return tickerlist
|
||||
|
||||
|
||||
def load_tickerdata_file(datadir, pair, ticker_interval,
|
||||
timerange=None):
|
||||
"""
|
||||
Load a pair from file,
|
||||
:return dict OR empty if unsuccesful
|
||||
@ -30,11 +45,13 @@ def load_tickerdata_file(datadir, pair, ticker_interval):
|
||||
# Read the file, load the json
|
||||
with open(file) as tickerdata:
|
||||
pairdata = json.load(tickerdata)
|
||||
if timerange:
|
||||
pairdata = trim_tickerlist(pairdata, timerange)
|
||||
return pairdata
|
||||
|
||||
|
||||
def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] = None,
|
||||
refresh_pairs: Optional[bool] = False) -> Dict[str, List]:
|
||||
def load_data(datadir: str, ticker_interval: int, pairs: Optional[List[str]] = None,
|
||||
refresh_pairs: Optional[bool] = False, timerange=None) -> Dict[str, List]:
|
||||
"""
|
||||
Loads ticker history data for the given parameters
|
||||
:param ticker_interval: ticker interval in minutes
|
||||
@ -48,19 +65,24 @@ def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]]
|
||||
# If the user force the refresh of pairs
|
||||
if refresh_pairs:
|
||||
logger.info('Download data for all pairs and store them in %s', datadir)
|
||||
download_pairs(datadir, _pairs)
|
||||
download_pairs(datadir, _pairs, ticker_interval)
|
||||
|
||||
for pair in _pairs:
|
||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval)
|
||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
||||
if not pairdata:
|
||||
# download the tickerdata from exchange
|
||||
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
|
||||
# and retry reading the pair
|
||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval)
|
||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
||||
result[pair] = pairdata
|
||||
return result
|
||||
|
||||
|
||||
def tickerdata_to_dataframe(data):
|
||||
preprocessed = preprocess(data)
|
||||
return preprocessed
|
||||
|
||||
|
||||
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
||||
"""Creates a dataframe and populates indicators for given ticker data"""
|
||||
return {pair: populate_indicators(parse_ticker_dataframe(pair_data))
|
||||
@ -73,21 +95,26 @@ def make_testdata_path(datadir: str) -> str:
|
||||
'..', 'tests', 'testdata'))
|
||||
|
||||
|
||||
def download_pairs(datadir, pairs: List[str]) -> bool:
|
||||
"""For each pairs passed in parameters, download 1 and 5 ticker intervals"""
|
||||
def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
|
||||
"""For each pairs passed in parameters, download the ticker intervals"""
|
||||
for pair in pairs:
|
||||
try:
|
||||
for interval in [1, 5]:
|
||||
download_backtesting_testdata(datadir, pair=pair, interval=interval)
|
||||
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
|
||||
except BaseException:
|
||||
logger.info('Failed to download the pair: "{pair}", Interval: {interval} min'.format(
|
||||
pair=pair,
|
||||
interval=interval,
|
||||
interval=ticker_interval,
|
||||
))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def file_dump_json(filename, data):
|
||||
with open(filename, "wt") as fp:
|
||||
json.dump(data, fp)
|
||||
|
||||
|
||||
# FIX: 20180110, suggest rename interval to tick_interval
|
||||
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool:
|
||||
"""
|
||||
Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters
|
||||
@ -107,7 +134,6 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
|
||||
pair=filepair,
|
||||
interval=interval,
|
||||
))
|
||||
filename = filename.replace('USDT_BTC', 'BTC_FAKEBULL')
|
||||
|
||||
if os.path.isfile(filename):
|
||||
with open(filename, "rt") as fp:
|
||||
@ -127,7 +153,6 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
|
||||
logger.debug("New End: {}".format(data[-1:][0]['T']))
|
||||
data = sorted(data, key=lambda data: data['T'])
|
||||
|
||||
with open(filename, "wt") as fp:
|
||||
json.dump(data, fp)
|
||||
misc.file_dump_json(filename, data)
|
||||
|
||||
return True
|
||||
|
@ -13,8 +13,8 @@ from freqtrade import exchange
|
||||
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
|
||||
from freqtrade.exchange import Bittrex
|
||||
from freqtrade.main import min_roi_reached
|
||||
from freqtrade.optimize import preprocess
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy.strategy import Strategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -67,17 +67,60 @@ def generate_text_table(
|
||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
|
||||
|
||||
|
||||
def backtest(stake_amount: float, processed: Dict[str, DataFrame],
|
||||
max_open_trades: int = 0, realistic: bool = True, sell_profit_only: bool = False,
|
||||
stoploss: int = -1.00, use_sell_signal: bool = False) -> DataFrame:
|
||||
def get_trade_entry(pair, row, ticker, trade_count_lock, args):
|
||||
stake_amount = args['stake_amount']
|
||||
max_open_trades = args.get('max_open_trades', 0)
|
||||
sell_profit_only = args.get('sell_profit_only', False)
|
||||
stoploss = args.get('stoploss', -1)
|
||||
use_sell_signal = args.get('use_sell_signal', False)
|
||||
trade = Trade(open_rate=row.close,
|
||||
open_date=row.date,
|
||||
stake_amount=stake_amount,
|
||||
amount=stake_amount / row.open,
|
||||
fee=exchange.get_fee()
|
||||
)
|
||||
|
||||
# calculate win/lose forwards from buy point
|
||||
sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']]
|
||||
for row2 in sell_subset.itertuples(index=True):
|
||||
if max_open_trades > 0:
|
||||
# Increase trade_count_lock for every iteration
|
||||
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
|
||||
|
||||
current_profit_percent = trade.calc_profit_percent(rate=row2.close)
|
||||
if (sell_profit_only and current_profit_percent < 0):
|
||||
continue
|
||||
if min_roi_reached(trade, row2.close, row2.date) or \
|
||||
(row2.sell == 1 and use_sell_signal) or \
|
||||
current_profit_percent <= stoploss:
|
||||
current_profit_btc = trade.calc_profit(rate=row2.close)
|
||||
return row2, (pair,
|
||||
current_profit_percent,
|
||||
current_profit_btc,
|
||||
row2.Index - row.Index,
|
||||
current_profit_btc > 0,
|
||||
current_profit_btc < 0
|
||||
)
|
||||
|
||||
|
||||
def backtest(args) -> DataFrame:
|
||||
"""
|
||||
Implements backtesting functionality
|
||||
:param stake_amount: btc amount to use for each trade
|
||||
:param processed: a processed dictionary with format {pair, data}
|
||||
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||
:param realistic: do we try to simulate realistic trades? (default: True)
|
||||
:param args: a dict containing:
|
||||
stake_amount: btc amount to use for each trade
|
||||
processed: a processed dictionary with format {pair, data}
|
||||
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||
realistic: do we try to simulate realistic trades? (default: True)
|
||||
sell_profit_only: sell if profit only
|
||||
use_sell_signal: act on sell-signal
|
||||
stoploss: use stoploss
|
||||
:return: DataFrame
|
||||
"""
|
||||
processed = args['processed']
|
||||
max_open_trades = args.get('max_open_trades', 0)
|
||||
realistic = args.get('realistic', True)
|
||||
record = args.get('record', None)
|
||||
records = []
|
||||
trades = []
|
||||
trade_count_lock: dict = {}
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
@ -100,41 +143,25 @@ def backtest(stake_amount: float, processed: Dict[str, DataFrame],
|
||||
# Increase lock
|
||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||
|
||||
trade = Trade(
|
||||
open_rate=row.close,
|
||||
open_date=row.date,
|
||||
stake_amount=stake_amount,
|
||||
amount=stake_amount / row.open,
|
||||
fee=exchange.get_fee()
|
||||
)
|
||||
|
||||
# calculate win/lose forwards from buy point
|
||||
sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']]
|
||||
for row2 in sell_subset.itertuples(index=True):
|
||||
if max_open_trades > 0:
|
||||
# Increase trade_count_lock for every iteration
|
||||
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
|
||||
|
||||
current_profit_percent = trade.calc_profit_percent(rate=row2.close)
|
||||
if (sell_profit_only and current_profit_percent < 0):
|
||||
continue
|
||||
if min_roi_reached(trade, row2.close, row2.date) or \
|
||||
(row2.sell == 1 and use_sell_signal) or \
|
||||
current_profit_percent <= stoploss:
|
||||
current_profit_btc = trade.calc_profit(rate=row2.close)
|
||||
lock_pair_until = row2.Index
|
||||
|
||||
trades.append(
|
||||
(
|
||||
pair,
|
||||
current_profit_percent,
|
||||
current_profit_btc,
|
||||
row2.Index - row.Index,
|
||||
current_profit_btc > 0,
|
||||
current_profit_btc < 0
|
||||
)
|
||||
)
|
||||
break
|
||||
ret = get_trade_entry(pair, row, ticker,
|
||||
trade_count_lock, args)
|
||||
if ret:
|
||||
row2, trade_entry = ret
|
||||
lock_pair_until = row2.Index
|
||||
trades.append(trade_entry)
|
||||
if record:
|
||||
# Note, need to be json.dump friendly
|
||||
# record a tuple of pair, current_profit_percent,
|
||||
# entry-date, duration
|
||||
records.append((pair, trade_entry[1],
|
||||
row.date.strftime('%s'),
|
||||
row2.date.strftime('%s'),
|
||||
row.Index, trade_entry[3]))
|
||||
# For now export inside backtest(), maybe change so that backtest()
|
||||
# returns a tuple like: (dataframe, records, logs, etc)
|
||||
if record and record.find('trades') >= 0:
|
||||
logger.info('Dumping backtest results')
|
||||
misc.file_dump_json('backtest-result.json', records)
|
||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'profit', 'loss']
|
||||
return DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
@ -150,47 +177,54 @@ def start(args):
|
||||
|
||||
logger.info('Using config: %s ...', args.config)
|
||||
config = misc.load_config(args.config)
|
||||
|
||||
logger.info('Using ticker_interval: %s ...', args.ticker_interval)
|
||||
ticker_interval = config.get('ticker_interval', args.ticker_interval)
|
||||
logger.info('Using ticker_interval: %s ...', ticker_interval)
|
||||
|
||||
data = {}
|
||||
pairs = config['exchange']['pair_whitelist']
|
||||
if args.live:
|
||||
logger.info('Downloading data for all pairs in whitelist ...')
|
||||
for pair in pairs:
|
||||
data[pair] = exchange.get_ticker_history(pair, args.ticker_interval)
|
||||
data[pair] = exchange.get_ticker_history(pair, ticker_interval)
|
||||
else:
|
||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||
data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval,
|
||||
refresh_pairs=args.refresh_pairs)
|
||||
|
||||
logger.info('Using stake_currency: %s ...', config['stake_currency'])
|
||||
logger.info('Using stake_amount: %s ...', config['stake_amount'])
|
||||
|
||||
timerange = misc.parse_timerange(args.timerange)
|
||||
data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval,
|
||||
refresh_pairs=args.refresh_pairs,
|
||||
timerange=timerange)
|
||||
max_open_trades = 0
|
||||
if args.realistic_simulation:
|
||||
logger.info('Using max_open_trades: %s ...', config['max_open_trades'])
|
||||
max_open_trades = config['max_open_trades']
|
||||
|
||||
# init the strategy to use
|
||||
config.update({'strategy': args.strategy})
|
||||
strategy = Strategy()
|
||||
strategy.init(config)
|
||||
|
||||
# Monkey patch config
|
||||
from freqtrade import main
|
||||
main._CONF = config
|
||||
|
||||
preprocessed = preprocess(data)
|
||||
preprocessed = optimize.tickerdata_to_dataframe(data)
|
||||
# Print timeframe
|
||||
min_date, max_date = get_timeframe(preprocessed)
|
||||
logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat())
|
||||
|
||||
# Execute backtest and print results
|
||||
results = backtest(
|
||||
stake_amount=config['stake_amount'],
|
||||
processed=preprocessed,
|
||||
max_open_trades=max_open_trades,
|
||||
realistic=args.realistic_simulation,
|
||||
sell_profit_only=config.get('experimental', {}).get('sell_profit_only', False),
|
||||
stoploss=config.get('stoploss'),
|
||||
use_sell_signal=config.get('experimental', {}).get('use_sell_signal', False)
|
||||
)
|
||||
sell_profit_only = config.get('experimental', {}).get('sell_profit_only', False)
|
||||
use_sell_signal = config.get('experimental', {}).get('use_sell_signal', False)
|
||||
results = backtest({'stake_amount': config['stake_amount'],
|
||||
'processed': preprocessed,
|
||||
'max_open_trades': max_open_trades,
|
||||
'realistic': args.realistic_simulation,
|
||||
'sell_profit_only': sell_profit_only,
|
||||
'use_sell_signal': use_sell_signal,
|
||||
'stoploss': strategy.stoploss,
|
||||
'record': args.export
|
||||
})
|
||||
logger.info(
|
||||
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa
|
||||
generate_text_table(data, results, config['stake_currency'], args.ticker_interval)
|
||||
|
@ -3,25 +3,31 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import pickle
|
||||
import signal
|
||||
import os
|
||||
import sys
|
||||
from functools import reduce
|
||||
from math import exp
|
||||
from operator import itemgetter
|
||||
from typing import Dict, List
|
||||
|
||||
import numpy
|
||||
import talib.abstract as ta
|
||||
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
|
||||
from hyperopt.mongoexp import MongoTrials
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import main # noqa
|
||||
from freqtrade import exchange, optimize
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
# Monkey patch config
|
||||
from freqtrade import main # noqa; noqa
|
||||
from freqtrade import exchange, misc, optimize
|
||||
from freqtrade.exchange import Bittrex
|
||||
from freqtrade.misc import load_config
|
||||
from freqtrade.optimize import backtesting
|
||||
from freqtrade.optimize.backtesting import backtest
|
||||
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
|
||||
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
||||
from freqtrade.strategy.strategy import Strategy
|
||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
||||
|
||||
# Remove noisy log messages
|
||||
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
|
||||
@ -49,63 +55,130 @@ PROCESSED = None # optimize.preprocess(optimize.load_data())
|
||||
OPTIMIZE_CONFIG = hyperopt_optimize_conf()
|
||||
|
||||
# Hyperopt Trials
|
||||
TRIALS_FILE = os.path.join('freqtrade', 'optimize', 'hyperopt_trials.pickle')
|
||||
TRIALS_FILE = os.path.join('user_data', 'hyperopt_trials.pickle')
|
||||
TRIALS = Trials()
|
||||
|
||||
# Monkey patch config
|
||||
from freqtrade import main # noqa
|
||||
main._CONF = OPTIMIZE_CONFIG
|
||||
|
||||
|
||||
SPACE = {
|
||||
'mfi': hp.choice('mfi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)}
|
||||
]),
|
||||
'fastd': hp.choice('fastd', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)}
|
||||
]),
|
||||
'adx': hp.choice('adx', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
|
||||
]),
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
||||
]),
|
||||
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'over_sar': hp.choice('over_sar', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'green_candle': hp.choice('green_candle', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'uptrend_sma': hp.choice('uptrend_sma', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'trigger': hp.choice('trigger', [
|
||||
{'type': 'lower_bb'},
|
||||
{'type': 'faststoch10'},
|
||||
{'type': 'ao_cross_zero'},
|
||||
{'type': 'ema5_cross_ema10'},
|
||||
{'type': 'macd_cross_signal'},
|
||||
{'type': 'sar_reversal'},
|
||||
{'type': 'stochf_cross'},
|
||||
{'type': 'ht_sine'},
|
||||
]),
|
||||
'stoploss': hp.uniform('stoploss', -0.5, -0.02),
|
||||
}
|
||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
"""
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
dataframe['roc'] = ta.ROC(dataframe)
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
# Stoch
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
# Stoch RSI
|
||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
# SAR Parabolic
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
# SMA - Simple Moving Average
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hammer: values [0, 100]
|
||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
# Inverted Hammer: values [0, 100]
|
||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
# Dragonfly Doji: values [0, 100]
|
||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
# Piercing Line: values [0, 100]
|
||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
# Morningstar: values [0, 100]
|
||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
# Three White Soldiers: values [0, 100]
|
||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hanging Man: values [0, 100]
|
||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
# Shooting Star: values [0, 100]
|
||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
# Gravestone Doji: values [0, 100]
|
||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
# Dark Cloud Cover: values [0, 100]
|
||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
# Evening Doji Star: values [0, 100]
|
||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
# Evening Star: values [0, 100]
|
||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Three Line Strike: values [0, -100, 100]
|
||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
# Spinning Top: values [0, -100, 100]
|
||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
# Engulfing: values [0, -100, 100]
|
||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
# Harami: values [0, -100, 100]
|
||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
# Three Outside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
# Three Inside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
"""
|
||||
|
||||
# Chart type
|
||||
# ------------------------------------
|
||||
# Heikinashi stategy
|
||||
heikinashi = qtpylib.heikinashi(dataframe)
|
||||
dataframe['ha_open'] = heikinashi['open']
|
||||
dataframe['ha_close'] = heikinashi['close']
|
||||
dataframe['ha_high'] = heikinashi['high']
|
||||
dataframe['ha_low'] = heikinashi['low']
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
def save_trials(trials, trials_path=TRIALS_FILE):
|
||||
@ -152,13 +225,183 @@ def calculate_loss(total_profit: float, trade_count: int, trade_duration: float)
|
||||
return trade_loss + profit_loss + duration_loss
|
||||
|
||||
|
||||
def generate_roi_table(params):
|
||||
roi_table = {}
|
||||
roi_table["0"] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
||||
roi_table[str(params['roi_t3'])] = params['roi_p1'] + params['roi_p2']
|
||||
roi_table[str(params['roi_t3'] + params['roi_t2'])] = params['roi_p1']
|
||||
roi_table[str(params['roi_t3'] + params['roi_t2'] + params['roi_t1'])] = 0
|
||||
|
||||
return roi_table
|
||||
|
||||
|
||||
def roi_space() -> List[Dict]:
|
||||
return {
|
||||
'roi_t1': hp.quniform('roi_t1', 10, 220, 10),
|
||||
'roi_t2': hp.quniform('roi_t2', 10, 120, 10),
|
||||
'roi_t3': hp.quniform('roi_t3', 10, 120, 10),
|
||||
'roi_p1': hp.quniform('roi_p1', 0.01, 0.05, 0.01),
|
||||
'roi_p2': hp.quniform('roi_p2', 0.01, 0.10, 0.01),
|
||||
'roi_p3': hp.quniform('roi_p3', 0.01, 0.30, 0.01),
|
||||
}
|
||||
|
||||
|
||||
def stoploss_space() -> Dict:
|
||||
return {
|
||||
'stoploss': hp.uniform('stoploss', -0.5, -0.02),
|
||||
}
|
||||
|
||||
|
||||
def indicator_space() -> List[Dict]:
|
||||
"""
|
||||
Define your Hyperopt space for searching strategy parameters
|
||||
"""
|
||||
return {
|
||||
'macd_below_zero': hp.choice('macd_below_zero', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'mfi': hp.choice('mfi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)}
|
||||
]),
|
||||
'fastd': hp.choice('fastd', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)}
|
||||
]),
|
||||
'adx': hp.choice('adx', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
|
||||
]),
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
||||
]),
|
||||
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'over_sar': hp.choice('over_sar', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'green_candle': hp.choice('green_candle', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'uptrend_sma': hp.choice('uptrend_sma', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'trigger': hp.choice('trigger', [
|
||||
{'type': 'lower_bb'},
|
||||
{'type': 'lower_bb_tema'},
|
||||
{'type': 'faststoch10'},
|
||||
{'type': 'ao_cross_zero'},
|
||||
{'type': 'ema3_cross_ema10'},
|
||||
{'type': 'macd_cross_signal'},
|
||||
{'type': 'sar_reversal'},
|
||||
{'type': 'ht_sine'},
|
||||
{'type': 'heiken_reversal_bull'},
|
||||
{'type': 'di_cross'},
|
||||
]),
|
||||
}
|
||||
|
||||
|
||||
def hyperopt_space() -> List[Dict]:
|
||||
return {**indicator_space(), **roi_space(), **stoploss_space()}
|
||||
|
||||
|
||||
def buy_strategy_generator(params) -> None:
|
||||
"""
|
||||
Define the buy strategy parameters to be used by hyperopt
|
||||
"""
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if 'uptrend_long_ema' in params and params['uptrend_long_ema']['enabled']:
|
||||
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
||||
if 'macd_below_zero' in params and params['macd_below_zero']['enabled']:
|
||||
conditions.append(dataframe['macd'] < 0)
|
||||
if 'uptrend_short_ema' in params and params['uptrend_short_ema']['enabled']:
|
||||
conditions.append(dataframe['ema5'] > dataframe['ema10'])
|
||||
if 'mfi' in params and params['mfi']['enabled']:
|
||||
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
||||
if 'fastd' in params and params['fastd']['enabled']:
|
||||
conditions.append(dataframe['fastd'] < params['fastd']['value'])
|
||||
if 'adx' in params and params['adx']['enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||
if 'rsi' in params and params['rsi']['enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||
if 'over_sar' in params and params['over_sar']['enabled']:
|
||||
conditions.append(dataframe['close'] > dataframe['sar'])
|
||||
if 'green_candle' in params and params['green_candle']['enabled']:
|
||||
conditions.append(dataframe['close'] > dataframe['open'])
|
||||
if 'uptrend_sma' in params and params['uptrend_sma']['enabled']:
|
||||
prevsma = dataframe['sma'].shift(1)
|
||||
conditions.append(dataframe['sma'] > prevsma)
|
||||
|
||||
# TRIGGERS
|
||||
triggers = {
|
||||
'lower_bb': (
|
||||
dataframe['close'] < dataframe['bb_lowerband']
|
||||
),
|
||||
'lower_bb_tema': (
|
||||
dataframe['tema'] < dataframe['bb_lowerband']
|
||||
),
|
||||
'faststoch10': (qtpylib.crossed_above(
|
||||
dataframe['fastd'], 10.0
|
||||
)),
|
||||
'ao_cross_zero': (qtpylib.crossed_above(
|
||||
dataframe['ao'], 0.0
|
||||
)),
|
||||
'ema3_cross_ema10': (qtpylib.crossed_above(
|
||||
dataframe['ema3'], dataframe['ema10']
|
||||
)),
|
||||
'macd_cross_signal': (qtpylib.crossed_above(
|
||||
dataframe['macd'], dataframe['macdsignal']
|
||||
)),
|
||||
'sar_reversal': (qtpylib.crossed_above(
|
||||
dataframe['close'], dataframe['sar']
|
||||
)),
|
||||
'ht_sine': (qtpylib.crossed_above(
|
||||
dataframe['htleadsine'], dataframe['htsine']
|
||||
)),
|
||||
'heiken_reversal_bull': (
|
||||
(qtpylib.crossed_above(dataframe['ha_close'], dataframe['ha_open'])) &
|
||||
(dataframe['ha_low'] == dataframe['ha_open'])
|
||||
),
|
||||
'di_cross': (qtpylib.crossed_above(
|
||||
dataframe['plus_di'], dataframe['minus_di']
|
||||
)),
|
||||
}
|
||||
conditions.append(triggers.get(params['trigger']['type']))
|
||||
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
return populate_buy_trend
|
||||
|
||||
|
||||
def optimizer(params):
|
||||
global _CURRENT_TRIES
|
||||
|
||||
from freqtrade.optimize import backtesting
|
||||
if 'roi_t1' in params:
|
||||
strategy = Strategy()
|
||||
strategy.minimal_roi = generate_roi_table(params)
|
||||
|
||||
backtesting.populate_buy_trend = buy_strategy_generator(params)
|
||||
|
||||
results = backtest(OPTIMIZE_CONFIG['stake_amount'], PROCESSED, stoploss=params['stoploss'])
|
||||
results = backtest({'stake_amount': OPTIMIZE_CONFIG['stake_amount'],
|
||||
'processed': PROCESSED,
|
||||
'stoploss': params['stoploss']})
|
||||
result_explanation = format_results(results)
|
||||
|
||||
total_profit = results.profit_percent.sum()
|
||||
@ -201,53 +444,8 @@ def format_results(results: DataFrame):
|
||||
)
|
||||
|
||||
|
||||
def buy_strategy_generator(params):
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if params['uptrend_long_ema']['enabled']:
|
||||
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
||||
if params['uptrend_short_ema']['enabled']:
|
||||
conditions.append(dataframe['ema5'] > dataframe['ema10'])
|
||||
if params['mfi']['enabled']:
|
||||
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
||||
if params['fastd']['enabled']:
|
||||
conditions.append(dataframe['fastd'] < params['fastd']['value'])
|
||||
if params['adx']['enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||
if params['rsi']['enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||
if params['over_sar']['enabled']:
|
||||
conditions.append(dataframe['close'] > dataframe['sar'])
|
||||
if params['green_candle']['enabled']:
|
||||
conditions.append(dataframe['close'] > dataframe['open'])
|
||||
if params['uptrend_sma']['enabled']:
|
||||
prevsma = dataframe['sma'].shift(1)
|
||||
conditions.append(dataframe['sma'] > prevsma)
|
||||
|
||||
# TRIGGERS
|
||||
triggers = {
|
||||
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
||||
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
|
||||
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
|
||||
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
|
||||
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
|
||||
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
|
||||
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
|
||||
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
|
||||
}
|
||||
conditions.append(triggers.get(params['trigger']['type']))
|
||||
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
return populate_buy_trend
|
||||
|
||||
|
||||
def start(args):
|
||||
global TOTAL_TRIES, PROCESSED, SPACE, TRIALS, _CURRENT_TRIES
|
||||
global TOTAL_TRIES, PROCESSED, TRIALS, _CURRENT_TRIES
|
||||
|
||||
TOTAL_TRIES = args.epochs
|
||||
|
||||
@ -262,8 +460,18 @@ def start(args):
|
||||
logger.info('Using config: %s ...', args.config)
|
||||
config = load_config(args.config)
|
||||
pairs = config['exchange']['pair_whitelist']
|
||||
PROCESSED = optimize.preprocess(optimize.load_data(
|
||||
args.datadir, pairs=pairs, ticker_interval=args.ticker_interval))
|
||||
|
||||
# init the strategy to use
|
||||
config.update({'strategy': args.strategy})
|
||||
strategy = Strategy()
|
||||
strategy.init(config)
|
||||
|
||||
timerange = misc.parse_timerange(args.timerange)
|
||||
data = optimize.load_data(args.datadir, pairs=pairs,
|
||||
ticker_interval=args.ticker_interval,
|
||||
timerange=timerange)
|
||||
optimize.populate_indicators = populate_indicators
|
||||
PROCESSED = optimize.tickerdata_to_dataframe(data)
|
||||
|
||||
if args.mongodb:
|
||||
logger.info('Using mongodb ...')
|
||||
@ -287,7 +495,7 @@ def start(args):
|
||||
try:
|
||||
best_parameters = fmin(
|
||||
fn=optimizer,
|
||||
space=SPACE,
|
||||
space=hyperopt_space(),
|
||||
algo=tpe.suggest,
|
||||
max_evals=TOTAL_TRIES,
|
||||
trials=TRIALS
|
||||
@ -303,9 +511,14 @@ def start(args):
|
||||
|
||||
# Improve best parameter logging display
|
||||
if best_parameters:
|
||||
best_parameters = space_eval(SPACE, best_parameters)
|
||||
best_parameters = space_eval(
|
||||
hyperopt_space(),
|
||||
best_parameters
|
||||
)
|
||||
|
||||
logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4))
|
||||
if 'roi_t1' in best_parameters:
|
||||
logger.info('ROI table:\n%s', generate_roi_table(best_parameters))
|
||||
logger.info('Best Result:\n%s', best_result)
|
||||
|
||||
# Store trials result to file to resume next time
|
||||
|
@ -47,6 +47,10 @@ def init(config: dict, engine: Optional[Engine] = None) -> None:
|
||||
Trade.query = session.query_property()
|
||||
_DECL_BASE.metadata.create_all(engine)
|
||||
|
||||
# Clean dry_run DB
|
||||
if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False):
|
||||
clean_dry_run_db()
|
||||
|
||||
|
||||
def cleanup() -> None:
|
||||
"""
|
||||
@ -56,6 +60,17 @@ def cleanup() -> None:
|
||||
Trade.session.flush()
|
||||
|
||||
|
||||
def clean_dry_run_db() -> None:
|
||||
"""
|
||||
Remove open_order_id from a Dry_run DB
|
||||
:return: None
|
||||
"""
|
||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||
# Check we are updating only a dry_run order not a prod one
|
||||
if 'dry_run' in trade.open_order_id:
|
||||
trade.open_order_id = None
|
||||
|
||||
|
||||
class Trade(_DECL_BASE):
|
||||
__tablename__ = 'trades'
|
||||
|
||||
|
@ -155,7 +155,6 @@ def rpc_daily_profit(timescale, stake_currency, fiat_display_currency):
|
||||
if not (isinstance(timescale, int) and timescale > 0):
|
||||
return (True, '*Daily [n]:* `must be an integer greater than 0`')
|
||||
|
||||
# FIX: we might not want to call CryptoToFiatConverter, for every call
|
||||
fiat = CryptoToFiatConverter()
|
||||
for day in range(0, timescale):
|
||||
profitday = today - timedelta(days=day)
|
||||
@ -166,20 +165,27 @@ def rpc_daily_profit(timescale, stake_currency, fiat_display_currency):
|
||||
.order_by(Trade.close_date)\
|
||||
.all()
|
||||
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
||||
profit_days[profitday] = format(curdayprofit, '.8f')
|
||||
profit_days[profitday] = {
|
||||
'amount': format(curdayprofit, '.8f'),
|
||||
'trades': len(trades)
|
||||
}
|
||||
|
||||
stats = [
|
||||
[
|
||||
key,
|
||||
'{value:.8f} {symbol}'.format(value=float(value), symbol=stake_currency),
|
||||
'{value:.8f} {symbol}'.format(
|
||||
value=float(value['amount']),
|
||||
symbol=stake_currency
|
||||
),
|
||||
'{value:.3f} {symbol}'.format(
|
||||
value=fiat.convert_amount(
|
||||
value,
|
||||
value['amount'],
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
),
|
||||
symbol=fiat_display_currency
|
||||
)
|
||||
),
|
||||
'{value} trade{s}'.format(value=value['trades'], s='' if value['trades'] < 2 else 's'),
|
||||
]
|
||||
for key, value in profit_days.items()
|
||||
]
|
||||
@ -296,6 +302,7 @@ def rpc_trade_statistics(stake_currency, fiat_display_currency) -> None:
|
||||
)
|
||||
return markdown_msg
|
||||
|
||||
|
||||
def rpc_balance(fiat_display_currency):
|
||||
"""
|
||||
:return: current account balance per crypto
|
||||
@ -326,4 +333,4 @@ def rpc_balance(fiat_display_currency):
|
||||
fiat = CryptoToFiatConverter()
|
||||
symbol = fiat_display_currency
|
||||
value = fiat.convert_amount(total, 'BTC', symbol)
|
||||
return (output, total, symbol, value)
|
||||
return (False, (output, total, symbol, value))
|
||||
|
@ -136,8 +136,8 @@ def _status(bot: Bot, update: Update) -> None:
|
||||
if error:
|
||||
send_msg(trades, bot=bot)
|
||||
else:
|
||||
for message in trades:
|
||||
send_msg(message, bot=bot)
|
||||
for trademsg in trades:
|
||||
send_msg(trademsg, bot=bot)
|
||||
|
||||
|
||||
@authorized_only
|
||||
@ -252,11 +252,11 @@ def _balance(bot: Bot, update: Update) -> None:
|
||||
(currencys, total, symbol, value) = result
|
||||
output = ''
|
||||
for currency in currencys:
|
||||
output += """*Currency*: {Currency}
|
||||
*Available*: {Available}
|
||||
*Balance*: {Balance}
|
||||
*Pending*: {Pending}
|
||||
*Est. BTC*: {BTC: .8f}
|
||||
output += """*Currency*: {currency}
|
||||
*Available*: {available}
|
||||
*Balance*: {balance}
|
||||
*Pending*: {pending}
|
||||
*Est. BTC*: {est_btc: .8f}
|
||||
|
||||
""".format(**currency)
|
||||
|
||||
|
0
freqtrade/strategy/__init__.py
Normal file
0
freqtrade/strategy/__init__.py
Normal file
238
freqtrade/strategy/default_strategy.py
Normal file
238
freqtrade/strategy/default_strategy.py
Normal file
@ -0,0 +1,238 @@
|
||||
import talib.abstract as ta
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from pandas import DataFrame
|
||||
|
||||
|
||||
class_name = 'DefaultStrategy'
|
||||
|
||||
|
||||
class DefaultStrategy(IStrategy):
|
||||
"""
|
||||
Default Strategy provided by freqtrade bot.
|
||||
You can override it with your own strategy
|
||||
"""
|
||||
|
||||
# Minimal ROI designed for the strategy
|
||||
minimal_roi = {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
}
|
||||
|
||||
# Optimal stoploss designed for the strategy
|
||||
stoploss = -0.10
|
||||
|
||||
# Optimal ticker interval for the strategy
|
||||
ticker_interval = 5
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
# ------------------------------------
|
||||
|
||||
# ADX
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
|
||||
# Awesome oscillator
|
||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
"""
|
||||
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
"""
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
|
||||
# MFI
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
|
||||
# Minus Directional Indicator / Movement
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# Plus Directional Indicator / Movement
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
"""
|
||||
# ROC
|
||||
dataframe['roc'] = ta.ROC(dataframe)
|
||||
"""
|
||||
# RSI
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
"""
|
||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
# Stoch
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
"""
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
"""
|
||||
# Stoch RSI
|
||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
"""
|
||||
|
||||
# Overlap Studies
|
||||
# ------------------------------------
|
||||
|
||||
# Previous Bollinger bands
|
||||
# Because ta.BBANDS implementation is broken with small numbers, it actually
|
||||
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
|
||||
# and use middle band instead.
|
||||
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
||||
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
|
||||
# SAR Parabol
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
|
||||
# SMA - Simple Moving Average
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
# Cycle Indicator
|
||||
# ------------------------------------
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hammer: values [0, 100]
|
||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
# Inverted Hammer: values [0, 100]
|
||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
# Dragonfly Doji: values [0, 100]
|
||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
# Piercing Line: values [0, 100]
|
||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
# Morningstar: values [0, 100]
|
||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
# Three White Soldiers: values [0, 100]
|
||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hanging Man: values [0, 100]
|
||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
# Shooting Star: values [0, 100]
|
||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
# Gravestone Doji: values [0, 100]
|
||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
# Dark Cloud Cover: values [0, 100]
|
||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
# Evening Doji Star: values [0, 100]
|
||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
# Evening Star: values [0, 100]
|
||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Three Line Strike: values [0, -100, 100]
|
||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
# Spinning Top: values [0, -100, 100]
|
||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
# Engulfing: values [0, -100, 100]
|
||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
# Harami: values [0, -100, 100]
|
||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
# Three Outside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
# Three Inside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
"""
|
||||
|
||||
# Chart type
|
||||
# ------------------------------------
|
||||
# Heikinashi stategy
|
||||
heikinashi = qtpylib.heikinashi(dataframe)
|
||||
dataframe['ha_open'] = heikinashi['open']
|
||||
dataframe['ha_close'] = heikinashi['close']
|
||||
dataframe['ha_high'] = heikinashi['high']
|
||||
dataframe['ha_low'] = heikinashi['low']
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['rsi'] < 35) &
|
||||
(dataframe['fastd'] < 35) &
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['plus_di'] > 0.5)
|
||||
) |
|
||||
(
|
||||
(dataframe['adx'] > 65) &
|
||||
(dataframe['plus_di'] > 0.5)
|
||||
),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(
|
||||
(qtpylib.crossed_above(dataframe['rsi'], 70)) |
|
||||
(qtpylib.crossed_above(dataframe['fastd'], 70))
|
||||
) &
|
||||
(dataframe['adx'] > 10) &
|
||||
(dataframe['minus_di'] > 0)
|
||||
) |
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['minus_di'] > 0.5)
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
44
freqtrade/strategy/interface.py
Normal file
44
freqtrade/strategy/interface.py
Normal file
@ -0,0 +1,44 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from pandas import DataFrame
|
||||
|
||||
|
||||
class IStrategy(ABC):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""
|
||||
Name of the strategy.
|
||||
:return: str representation of the class name
|
||||
"""
|
||||
return self.__class__.__name__
|
||||
|
||||
"""
|
||||
Attributes you can use:
|
||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||
stoploss -> float: optimal stoploss designed for the strategy
|
||||
ticker_interval -> int: value of the ticker interval to use for the strategy
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Populate indicators that will be used in the Buy and Sell strategy
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
:return:
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
166
freqtrade/strategy/strategy.py
Normal file
166
freqtrade/strategy/strategy.py
Normal file
@ -0,0 +1,166 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import importlib
|
||||
|
||||
from pandas import DataFrame
|
||||
from typing import Dict
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
|
||||
sys.path.insert(0, r'../../user_data/strategies')
|
||||
|
||||
|
||||
class Strategy(object):
|
||||
__instance = None
|
||||
|
||||
DEFAULT_STRATEGY = 'default_strategy'
|
||||
|
||||
def __new__(cls):
|
||||
if Strategy.__instance is None:
|
||||
Strategy.__instance = object.__new__(cls)
|
||||
return Strategy.__instance
|
||||
|
||||
def init(self, config):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||
if 'strategy' in config:
|
||||
strategy = config['strategy']
|
||||
else:
|
||||
strategy = self.DEFAULT_STRATEGY
|
||||
|
||||
# Load the strategy
|
||||
self._load_strategy(strategy)
|
||||
|
||||
# Set attributes
|
||||
# Check if we need to override configuration
|
||||
if 'minimal_roi' in config:
|
||||
self.custom_strategy.minimal_roi = config['minimal_roi']
|
||||
self.logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
||||
|
||||
if 'stoploss' in config:
|
||||
self.custom_strategy.stoploss = config['stoploss']
|
||||
self.logger.info(
|
||||
"Override strategy \'stoploss\' with value in config file: {}.".format(
|
||||
config['stoploss']
|
||||
)
|
||||
)
|
||||
|
||||
if 'ticker_interval' in config:
|
||||
self.custom_strategy.ticker_interval = config['ticker_interval']
|
||||
self.logger.info(
|
||||
"Override strategy \'ticker_interval\' with value in config file: {}.".format(
|
||||
config['ticker_interval']
|
||||
)
|
||||
)
|
||||
|
||||
self.minimal_roi = self.custom_strategy.minimal_roi
|
||||
self.stoploss = self.custom_strategy.stoploss
|
||||
self.ticker_interval = self.custom_strategy.ticker_interval
|
||||
|
||||
def _load_strategy(self, strategy_name: str) -> None:
|
||||
"""
|
||||
Search and load the custom strategy. If no strategy found, fallback on the default strategy
|
||||
Set the object into self.custom_strategy
|
||||
:param strategy_name: name of the module to import
|
||||
:return: None
|
||||
"""
|
||||
|
||||
try:
|
||||
# Start by sanitizing the file name (remove any extensions)
|
||||
strategy_name = self._sanitize_module_name(filename=strategy_name)
|
||||
|
||||
# Search where can be the strategy file
|
||||
path = self._search_strategy(filename=strategy_name)
|
||||
|
||||
# Load the strategy
|
||||
self.custom_strategy = self._load_class(path + strategy_name)
|
||||
|
||||
# Fallback to the default strategy
|
||||
except (ImportError, TypeError):
|
||||
self.custom_strategy = self._load_class('.' + self.DEFAULT_STRATEGY)
|
||||
|
||||
def _load_class(self, filename: str) -> IStrategy:
|
||||
"""
|
||||
Import a strategy as a module
|
||||
:param filename: path to the strategy (path from freqtrade/strategy/)
|
||||
:return: return the strategy class
|
||||
"""
|
||||
module = importlib.import_module(filename, __package__)
|
||||
custom_strategy = getattr(module, module.class_name)
|
||||
|
||||
self.logger.info("Load strategy class: {} ({}.py)".format(module.class_name, filename))
|
||||
return custom_strategy()
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_module_name(filename: str) -> str:
|
||||
"""
|
||||
Remove any extension from filename
|
||||
:param filename: filename to sanatize
|
||||
:return: return the filename without extensions
|
||||
"""
|
||||
filename = os.path.basename(filename)
|
||||
filename = os.path.splitext(filename)[0]
|
||||
return filename
|
||||
|
||||
@staticmethod
|
||||
def _search_strategy(filename: str) -> str:
|
||||
"""
|
||||
Search for the Strategy file in different folder
|
||||
1. search into the user_data/strategies folder
|
||||
2. search into the freqtrade/strategy folder
|
||||
3. if nothing found, return None
|
||||
:param strategy_name: module name to search
|
||||
:return: module path where is the strategy
|
||||
"""
|
||||
pwd = os.path.dirname(os.path.realpath(__file__)) + '/'
|
||||
user_data = os.path.join(pwd, '..', '..', 'user_data', 'strategies', filename + '.py')
|
||||
strategy_folder = os.path.join(pwd, filename + '.py')
|
||||
|
||||
path = None
|
||||
if os.path.isfile(user_data):
|
||||
path = 'user_data.strategies.'
|
||||
elif os.path.isfile(strategy_folder):
|
||||
path = '.'
|
||||
|
||||
return path
|
||||
|
||||
def minimal_roi(self) -> Dict:
|
||||
"""
|
||||
Minimal ROI designed for the strategy
|
||||
:return: Dict: Value for the Minimal ROI
|
||||
"""
|
||||
return
|
||||
|
||||
def stoploss(self) -> float:
|
||||
"""
|
||||
Optimal stoploss designed for the strategy
|
||||
:return: float | return None to disable it
|
||||
"""
|
||||
return self.custom_strategy.stoploss
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Populate indicators that will be used in the Buy and Sell strategy
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
return self.custom_strategy.populate_indicators(dataframe)
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
:return:
|
||||
"""
|
||||
return self.custom_strategy.populate_buy_trend(dataframe)
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
return self.custom_strategy.populate_sell_trend(dataframe)
|
@ -1,6 +1,7 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
from functools import reduce
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
@ -10,6 +11,14 @@ from telegram import Chat, Message, Update
|
||||
from freqtrade.misc import CONF_SCHEMA
|
||||
|
||||
|
||||
def log_has(line, logs):
|
||||
# caplog mocker returns log as a tuple: ('freqtrade.analyze', logging.WARNING, 'foobar')
|
||||
# and we want to match line against foobar in the tuple
|
||||
return reduce(lambda a, b: a or b,
|
||||
filter(lambda x: x[2] == line, logs),
|
||||
False)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def default_conf():
|
||||
""" Returns validated configuration suitable for most tests """
|
||||
@ -18,6 +27,7 @@ def default_conf():
|
||||
"stake_currency": "BTC",
|
||||
"stake_amount": 0.001,
|
||||
"fiat_display_currency": "USD",
|
||||
"ticker_interval": 5,
|
||||
"dry_run": True,
|
||||
"minimal_roi": {
|
||||
"40": 0.0,
|
||||
@ -216,3 +226,33 @@ def ticker_history():
|
||||
"BV": 0.7039405
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ticker_history_without_bv():
|
||||
return [
|
||||
{
|
||||
"O": 8.794e-05,
|
||||
"H": 8.948e-05,
|
||||
"L": 8.794e-05,
|
||||
"C": 8.88e-05,
|
||||
"V": 991.09056638,
|
||||
"T": "2017-11-26T08:50:00"
|
||||
},
|
||||
{
|
||||
"O": 8.88e-05,
|
||||
"H": 8.942e-05,
|
||||
"L": 8.88e-05,
|
||||
"C": 8.893e-05,
|
||||
"V": 658.77935965,
|
||||
"T": "2017-11-26T08:55:00"
|
||||
},
|
||||
{
|
||||
"O": 8.891e-05,
|
||||
"H": 8.893e-05,
|
||||
"L": 8.875e-05,
|
||||
"C": 8.877e-05,
|
||||
"V": 7920.73570705,
|
||||
"T": "2017-11-26T09:00:00"
|
||||
}
|
||||
]
|
||||
|
@ -7,13 +7,23 @@ import pytest
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
|
||||
get_ticker, cancel_order, get_name, get_fee
|
||||
get_ticker, get_ticker_history, cancel_order, get_name, get_fee
|
||||
import freqtrade.exchange as exchange
|
||||
|
||||
API_INIT = False
|
||||
|
||||
|
||||
def maybe_init_api(conf, mocker):
|
||||
global API_INIT
|
||||
if not API_INIT:
|
||||
mocker.patch('freqtrade.exchange.validate_pairs',
|
||||
side_effect=lambda s: True)
|
||||
init(config=conf)
|
||||
API_INIT = True
|
||||
|
||||
|
||||
def test_init(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.exchange.validate_pairs',
|
||||
side_effect=lambda s: True)
|
||||
init(config=default_conf)
|
||||
maybe_init_api(default_conf, mocker)
|
||||
assert ('freqtrade.exchange',
|
||||
logging.INFO,
|
||||
'Instance is running with dry_run enabled'
|
||||
@ -159,8 +169,10 @@ def test_get_balances_prod(default_conf, mocker):
|
||||
assert get_balances()[0]['Pending'] == 0.0
|
||||
|
||||
|
||||
def test_get_ticker(mocker, ticker):
|
||||
|
||||
# This test is somewhat redundant with
|
||||
# test_exchange_bittrex.py::test_exchange_bittrex_get_ticker
|
||||
def test_get_ticker(default_conf, mocker, ticker):
|
||||
maybe_init_api(default_conf, mocker)
|
||||
api_mock = MagicMock()
|
||||
tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}}
|
||||
api_mock.get_ticker = MagicMock(return_value=tick)
|
||||
@ -177,6 +189,7 @@ def test_get_ticker(mocker, ticker):
|
||||
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
|
||||
|
||||
# if not caching the result we should get the same ticker
|
||||
# if not fetching a new result we should get the cached ticker
|
||||
ticker = get_ticker(pair='BTC_ETH', refresh=False)
|
||||
assert ticker['bid'] == 0.00001098
|
||||
assert ticker['ask'] == 0.00001099
|
||||
@ -187,6 +200,26 @@ def test_get_ticker(mocker, ticker):
|
||||
assert ticker['ask'] == 1
|
||||
|
||||
|
||||
def test_get_ticker_history(default_conf, mocker, ticker):
|
||||
api_mock = MagicMock()
|
||||
tick = 123
|
||||
api_mock.get_ticker_history = MagicMock(return_value=tick)
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
|
||||
# retrieve original ticker
|
||||
ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
|
||||
assert ticks == 123
|
||||
|
||||
# change the ticker
|
||||
tick = 999
|
||||
api_mock.get_ticker_history = MagicMock(return_value=tick)
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
|
||||
# ensure caching will still return the original ticker
|
||||
ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
|
||||
assert ticks == 123
|
||||
|
||||
|
||||
def test_cancel_order_dry_run(default_conf, mocker):
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
@ -194,6 +227,33 @@ def test_cancel_order_dry_run(default_conf, mocker):
|
||||
assert cancel_order(order_id='123') is None
|
||||
|
||||
|
||||
# Ensure that if not dry_run, we should call API
|
||||
def test_cancel_order(default_conf, mocker):
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
api_mock = MagicMock()
|
||||
api_mock.cancel_order = MagicMock(return_value=123)
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
assert cancel_order(order_id='_') == 123
|
||||
|
||||
|
||||
def test_get_order(default_conf, mocker):
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
order = MagicMock()
|
||||
order.myid = 123
|
||||
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
|
||||
print(exchange.get_order('X'))
|
||||
assert exchange.get_order('X').myid == 123
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
api_mock = MagicMock()
|
||||
api_mock.get_order = MagicMock(return_value=456)
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
assert 456 == exchange.get_order('X')
|
||||
|
||||
|
||||
def test_get_name(default_conf, mocker):
|
||||
mocker.patch('freqtrade.exchange.validate_pairs',
|
||||
side_effect=lambda s: True)
|
||||
@ -209,3 +269,18 @@ def test_get_fee(default_conf, mocker):
|
||||
init(default_conf)
|
||||
|
||||
assert get_fee() == 0.0025
|
||||
|
||||
|
||||
def test_exchange_misc(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
exchange.get_markets()
|
||||
assert 1 == api_mock.get_markets.call_count
|
||||
exchange.get_market_summaries()
|
||||
assert 1 == api_mock.get_market_summaries.call_count
|
||||
api_mock.name = 123
|
||||
assert 123 == exchange.get_name()
|
||||
api_mock.fee = 456
|
||||
assert 456 == exchange.get_fee()
|
||||
exchange.get_wallet_health()
|
||||
assert 1 == api_mock.get_wallet_health.call_count
|
||||
|
@ -143,7 +143,7 @@ def test_exchange_bittrex_fee():
|
||||
assert fee >= 0 and fee < 0.1 # Fee is 0-10 %
|
||||
|
||||
|
||||
def test_exchange_bittrex_buy_good(mocker):
|
||||
def test_exchange_bittrex_buy_good():
|
||||
wb = make_wrap_bittrex()
|
||||
fb = FakeBittrex()
|
||||
uuid = wb.buy('BTC_ETH', 1, 1)
|
||||
@ -154,7 +154,7 @@ def test_exchange_bittrex_buy_good(mocker):
|
||||
wb.buy('BAD', 1, 1)
|
||||
|
||||
|
||||
def test_exchange_bittrex_sell_good(mocker):
|
||||
def test_exchange_bittrex_sell_good():
|
||||
wb = make_wrap_bittrex()
|
||||
fb = FakeBittrex()
|
||||
uuid = wb.sell('BTC_ETH', 1, 1)
|
||||
@ -165,7 +165,7 @@ def test_exchange_bittrex_sell_good(mocker):
|
||||
uuid = wb.sell('BAD', 1, 1)
|
||||
|
||||
|
||||
def test_exchange_bittrex_get_balance(mocker):
|
||||
def test_exchange_bittrex_get_balance():
|
||||
wb = make_wrap_bittrex()
|
||||
fb = FakeBittrex()
|
||||
bal = wb.get_balance('BTC_ETH')
|
||||
@ -232,11 +232,18 @@ def test_exchange_bittrex_get_ticker_bad():
|
||||
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
|
||||
wb.get_ticker('BTC_ETH')
|
||||
|
||||
fb.result = {'success': True,
|
||||
'result': {'Bid': 1, 'Ask': 0, 'Last': None}} # incomplete result
|
||||
with pytest.raises(ContentDecodingError, match=r'.*Got invalid response from bittrex params.*'):
|
||||
wb.get_ticker('BTC_ETH')
|
||||
|
||||
def test_exchange_bittrex_get_ticker_history_one():
|
||||
|
||||
def test_exchange_bittrex_get_ticker_history_intervals():
|
||||
wb = make_wrap_bittrex()
|
||||
FakeBittrex()
|
||||
assert wb.get_ticker_history('BTC_ETH', 1)
|
||||
for tick_interval in [1, 5, 30, 60, 1440]:
|
||||
assert ([{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}] ==
|
||||
wb.get_ticker_history('BTC_ETH', tick_interval))
|
||||
|
||||
|
||||
def test_exchange_bittrex_get_ticker_history():
|
||||
@ -346,3 +353,8 @@ def test_validate_response_min_trade_requirement_not_met():
|
||||
}
|
||||
with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'):
|
||||
Bittrex._validate_response(response)
|
||||
|
||||
|
||||
def test_custom_requests(mocker):
|
||||
mocker.patch('freqtrade.exchange.bittrex.requests', MagicMock())
|
||||
btx.custom_requests('http://', '')
|
||||
|
@ -3,12 +3,28 @@
|
||||
import logging
|
||||
import math
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from freqtrade import exchange, optimize
|
||||
from freqtrade.exchange import Bittrex
|
||||
from freqtrade.optimize import preprocess
|
||||
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe
|
||||
import freqtrade.optimize.backtesting as backtesting
|
||||
from freqtrade.strategy.strategy import Strategy
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def default_strategy():
|
||||
strategy = Strategy()
|
||||
strategy.init({'strategy': 'default_strategy'})
|
||||
return strategy
|
||||
|
||||
|
||||
def trim_dictlist(dl, num):
|
||||
new = {}
|
||||
for pair, pair_data in dl.items():
|
||||
new[pair] = pair_data[num:]
|
||||
return new
|
||||
|
||||
|
||||
def test_generate_text_table():
|
||||
@ -30,7 +46,7 @@ def test_generate_text_table():
|
||||
'TOTAL 2 15.00 0.60000000 100.0 2 0') # noqa
|
||||
|
||||
|
||||
def test_get_timeframe():
|
||||
def test_get_timeframe(default_strategy):
|
||||
data = preprocess(optimize.load_data(
|
||||
None, ticker_interval=1, pairs=['BTC_UNITEST']))
|
||||
min_date, max_date = get_timeframe(data)
|
||||
@ -38,37 +54,36 @@ def test_get_timeframe():
|
||||
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
|
||||
|
||||
|
||||
def test_backtest(default_conf, mocker):
|
||||
def test_backtest(default_strategy, default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
|
||||
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
||||
results = backtest(default_conf['stake_amount'],
|
||||
optimize.preprocess(data), 10, True)
|
||||
data = trim_dictlist(data, -200)
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 10,
|
||||
'realistic': True})
|
||||
assert not results.empty
|
||||
|
||||
|
||||
def test_backtest_1min_ticker_interval(default_conf, mocker):
|
||||
def test_backtest_1min_ticker_interval(default_strategy, default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
|
||||
# Run a backtesting for an exiting 5min ticker_interval
|
||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
|
||||
results = backtest(default_conf['stake_amount'],
|
||||
optimize.preprocess(data), 1, True)
|
||||
data = trim_dictlist(data, -200)
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 1,
|
||||
'realistic': True})
|
||||
assert not results.empty
|
||||
|
||||
|
||||
def trim_dictlist(dl, num):
|
||||
new = {}
|
||||
for pair, pair_data in dl.items():
|
||||
new[pair] = pair_data[num:]
|
||||
return new
|
||||
|
||||
|
||||
def load_data_test(what):
|
||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
|
||||
data = trim_dictlist(data, -100)
|
||||
timerange = ((None, 'line'), None, -100)
|
||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange)
|
||||
pair = data['BTC_UNITEST']
|
||||
datalen = len(pair)
|
||||
# Depending on the what parameter we now adjust the
|
||||
@ -113,7 +128,10 @@ def simple_backtest(config, contour, num_results):
|
||||
data = load_data_test(contour)
|
||||
processed = optimize.preprocess(data)
|
||||
assert isinstance(processed, dict)
|
||||
results = backtest(config['stake_amount'], processed, 1, True)
|
||||
results = backtest({'stake_amount': config['stake_amount'],
|
||||
'processed': processed,
|
||||
'max_open_trades': 1,
|
||||
'realistic': True})
|
||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||
assert len(results) == num_results
|
||||
|
||||
@ -122,15 +140,18 @@ def simple_backtest(config, contour, num_results):
|
||||
# loaded by freqdata/optimize/__init__.py::load_data()
|
||||
|
||||
|
||||
def test_backtest2(default_conf, mocker):
|
||||
def test_backtest2(default_conf, mocker, default_strategy):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
||||
results = backtest(default_conf['stake_amount'],
|
||||
optimize.preprocess(data), 10, True)
|
||||
data = trim_dictlist(data, -200)
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 10,
|
||||
'realistic': True})
|
||||
assert not results.empty
|
||||
|
||||
|
||||
def test_processed(default_conf, mocker):
|
||||
def test_processed(default_conf, mocker, default_strategy):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
dict_of_tickerrows = load_data_test('raise')
|
||||
dataframes = optimize.preprocess(dict_of_tickerrows)
|
||||
@ -142,17 +163,17 @@ def test_processed(default_conf, mocker):
|
||||
assert col in cols
|
||||
|
||||
|
||||
def test_backtest_pricecontours(default_conf, mocker):
|
||||
def test_backtest_pricecontours(default_conf, mocker, default_strategy):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
|
||||
for [contour, numres] in tests:
|
||||
simple_backtest(default_conf, contour, numres)
|
||||
|
||||
|
||||
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False):
|
||||
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1)
|
||||
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None):
|
||||
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange)
|
||||
pairdata = {'BTC_UNITEST': tickerdata}
|
||||
return trim_dictlist(pairdata, -100)
|
||||
return pairdata
|
||||
|
||||
|
||||
def test_backtest_start(default_conf, mocker, caplog):
|
||||
@ -166,6 +187,8 @@ def test_backtest_start(default_conf, mocker, caplog):
|
||||
args.level = 10
|
||||
args.live = False
|
||||
args.datadir = None
|
||||
args.export = None
|
||||
args.timerange = '-100' # needed due to MagicMock malleability
|
||||
backtesting.start(args)
|
||||
# check the logs, that will contain the backtest result
|
||||
exists = ['Using max_open_trades: 1 ...',
|
||||
|
@ -1,6 +1,6 @@
|
||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||
from freqtrade.optimize.hyperopt import calculate_loss, TARGET_TRADES, EXPECTED_MAX_PROFIT, start, \
|
||||
log_results, save_trials, read_trials
|
||||
log_results, save_trials, read_trials, generate_roi_table
|
||||
|
||||
|
||||
def test_loss_calculation_prefer_correct_trade_count():
|
||||
@ -54,6 +54,7 @@ def create_trials(mocker):
|
||||
|
||||
def test_start_calls_fmin(mocker):
|
||||
trials = create_trials(mocker)
|
||||
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe')
|
||||
mocker.patch('freqtrade.optimize.hyperopt.TRIALS', return_value=trials)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.sorted',
|
||||
return_value=trials.results)
|
||||
@ -61,7 +62,8 @@ def test_start_calls_fmin(mocker):
|
||||
mocker.patch('freqtrade.optimize.load_data')
|
||||
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||
|
||||
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False)
|
||||
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False,
|
||||
timerange=None)
|
||||
start(args)
|
||||
|
||||
mock_fmin.assert_called_once()
|
||||
@ -70,11 +72,12 @@ def test_start_calls_fmin(mocker):
|
||||
def test_start_uses_mongotrials(mocker):
|
||||
mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials',
|
||||
return_value=create_trials(mocker))
|
||||
mocker.patch('freqtrade.optimize.preprocess')
|
||||
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe')
|
||||
mocker.patch('freqtrade.optimize.load_data')
|
||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||
|
||||
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True)
|
||||
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True,
|
||||
timerange=None)
|
||||
start(args)
|
||||
|
||||
mock_mongotrials.assert_called_once()
|
||||
@ -107,6 +110,7 @@ def test_no_log_if_loss_does_not_improve(mocker):
|
||||
|
||||
def test_fmin_best_results(mocker, caplog):
|
||||
fmin_result = {
|
||||
"macd_below_zero": 0,
|
||||
"adx": 1,
|
||||
"adx-value": 15.0,
|
||||
"fastd": 1,
|
||||
@ -121,14 +125,21 @@ def test_fmin_best_results(mocker, caplog):
|
||||
"uptrend_short_ema": 0,
|
||||
"uptrend_sma": 0,
|
||||
"stoploss": -0.1,
|
||||
"roi_t1": 1,
|
||||
"roi_t2": 2,
|
||||
"roi_t3": 3,
|
||||
"roi_p1": 1,
|
||||
"roi_p2": 2,
|
||||
"roi_p3": 3,
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
|
||||
mocker.patch('freqtrade.optimize.preprocess')
|
||||
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe')
|
||||
mocker.patch('freqtrade.optimize.load_data')
|
||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
||||
|
||||
args = mocker.Mock(epochs=1, config='config.json.example')
|
||||
args = mocker.Mock(epochs=1, config='config.json.example',
|
||||
timerange=None)
|
||||
start(args)
|
||||
|
||||
exists = [
|
||||
@ -136,7 +147,7 @@ def test_fmin_best_results(mocker, caplog):
|
||||
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
|
||||
'"green_candle": {\n "enabled": true\n },',
|
||||
'"mfi": {\n "enabled": false\n },',
|
||||
'"trigger": {\n "type": "ao_cross_zero"\n },',
|
||||
'"trigger": {\n "type": "faststoch10"\n },',
|
||||
'"stoploss": -0.1',
|
||||
]
|
||||
|
||||
@ -146,11 +157,12 @@ def test_fmin_best_results(mocker, caplog):
|
||||
|
||||
def test_fmin_throw_value_error(mocker, caplog):
|
||||
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
|
||||
mocker.patch('freqtrade.optimize.preprocess')
|
||||
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe')
|
||||
mocker.patch('freqtrade.optimize.load_data')
|
||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
|
||||
|
||||
args = mocker.Mock(epochs=1, config='config.json.example')
|
||||
args = mocker.Mock(epochs=1, config='config.json.example',
|
||||
timerange=None)
|
||||
start(args)
|
||||
|
||||
exists = [
|
||||
@ -184,7 +196,8 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker):
|
||||
return_value={})
|
||||
args = mocker.Mock(epochs=1,
|
||||
config='config.json.example',
|
||||
mongodb=False)
|
||||
mongodb=False,
|
||||
timerange=None)
|
||||
|
||||
start(args)
|
||||
|
||||
@ -221,3 +234,15 @@ def test_read_trials_returns_trials_file(mocker):
|
||||
assert read_trials() == trials
|
||||
mock_open.assert_called_once()
|
||||
mock_load.assert_called_once()
|
||||
|
||||
|
||||
def test_roi_table_generation():
|
||||
params = {
|
||||
'roi_t1': 5,
|
||||
'roi_t2': 10,
|
||||
'roi_t3': 15,
|
||||
'roi_p1': 1,
|
||||
'roi_p2': 2,
|
||||
'roi_p3': 3,
|
||||
}
|
||||
assert generate_roi_table(params) == {'0': 6, '15': 3, '25': 1, '30': 0}
|
||||
|
@ -1,6 +1,6 @@
|
||||
# pragma pylint: disable=missing-docstring,W0212
|
||||
|
||||
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
|
||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
||||
|
||||
|
||||
def test_hyperopt_optimize_conf():
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import os
|
||||
import logging
|
||||
# from unittest.mock import MagicMock
|
||||
from shutil import copyfile
|
||||
from freqtrade import exchange, optimize
|
||||
from freqtrade.exchange import Bittrex
|
||||
@ -43,6 +44,23 @@ def _clean_test_file(file: str) -> None:
|
||||
os.rename(file_swp, file)
|
||||
|
||||
|
||||
def test_load_data_30min_ticker(default_conf, ticker_history, mocker, caplog):
|
||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
|
||||
file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json'
|
||||
_backup_file(file, copy_file=True)
|
||||
optimize.load_data(None, pairs=['BTC_UNITTEST'], ticker_interval=30)
|
||||
assert os.path.isfile(file) is True
|
||||
assert ('freqtrade.optimize',
|
||||
logging.INFO,
|
||||
'Download the pair: "BTC_ETH", Interval: 30 min'
|
||||
) not in caplog.record_tuples
|
||||
_clean_test_file(file)
|
||||
|
||||
|
||||
def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
|
||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
@ -51,7 +69,7 @@ def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
|
||||
|
||||
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
|
||||
_backup_file(file, copy_file=True)
|
||||
optimize.load_data(None, pairs=['BTC_ETH'])
|
||||
optimize.load_data(None, pairs=['BTC_ETH'], ticker_interval=5)
|
||||
assert os.path.isfile(file) is True
|
||||
assert ('freqtrade.optimize',
|
||||
logging.INFO,
|
||||
@ -113,17 +131,28 @@ def test_download_pairs(default_conf, ticker_history, mocker):
|
||||
_backup_file(file2_1)
|
||||
_backup_file(file2_5)
|
||||
|
||||
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI']) is True
|
||||
assert os.path.isfile(file1_1) is False
|
||||
assert os.path.isfile(file2_1) is False
|
||||
|
||||
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI'], ticker_interval=1) is True
|
||||
|
||||
assert os.path.isfile(file1_1) is True
|
||||
assert os.path.isfile(file1_5) is True
|
||||
assert os.path.isfile(file2_1) is True
|
||||
assert os.path.isfile(file2_5) is True
|
||||
|
||||
# clean files freshly downloaded
|
||||
_clean_test_file(file1_1)
|
||||
_clean_test_file(file1_5)
|
||||
_clean_test_file(file2_1)
|
||||
|
||||
assert os.path.isfile(file1_5) is False
|
||||
assert os.path.isfile(file2_5) is False
|
||||
|
||||
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI'], ticker_interval=5) is True
|
||||
|
||||
assert os.path.isfile(file1_5) is True
|
||||
assert os.path.isfile(file2_5) is True
|
||||
|
||||
# clean files freshly downloaded
|
||||
_clean_test_file(file1_5)
|
||||
_clean_test_file(file2_5)
|
||||
|
||||
|
||||
@ -139,7 +168,7 @@ def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
|
||||
_backup_file(file1_1)
|
||||
_backup_file(file1_5)
|
||||
|
||||
download_pairs(None, pairs=['BTC-MEME'])
|
||||
download_pairs(None, pairs=['BTC-MEME'], ticker_interval=1)
|
||||
# clean files freshly downloaded
|
||||
_clean_test_file(file1_1)
|
||||
_clean_test_file(file1_5)
|
||||
@ -170,7 +199,30 @@ def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
|
||||
_clean_test_file(file2)
|
||||
|
||||
|
||||
def test_download_backtesting_testdata2(default_conf, mocker):
|
||||
tick = [{'T': 'bar'}, {'T': 'foo'}]
|
||||
mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
|
||||
assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=1)
|
||||
assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
|
||||
|
||||
|
||||
def test_load_tickerdata_file():
|
||||
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
|
||||
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
||||
assert _btc_unittest_length == len(tickerdata)
|
||||
|
||||
|
||||
def test_init(default_conf, mocker):
|
||||
conf = {'exchange': {'pair_whitelist': []}}
|
||||
mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf)
|
||||
assert {} == optimize.load_data('', pairs=[], refresh_pairs=True,
|
||||
ticker_interval=int(default_conf['ticker_interval']))
|
||||
|
||||
|
||||
def test_tickerdata_to_dataframe():
|
||||
timerange = ((None, 'line'), None, -100)
|
||||
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
|
||||
tickerlist = {'BTC_UNITEST': tick}
|
||||
data = optimize.tickerdata_to_dataframe(tickerlist)
|
||||
assert 100 == len(data['BTC_UNITEST'])
|
||||
|
@ -72,7 +72,7 @@ def test_send_msg_telegram_disabled(mocker):
|
||||
|
||||
def test_rpc_trade_status(default_conf, update, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
_CONF=default_conf,
|
||||
@ -92,7 +92,7 @@ def test_rpc_trade_status(default_conf, update, ticker, mocker):
|
||||
assert error
|
||||
assert result.find('no active trade') >= 0
|
||||
|
||||
main.create_trade(0.001)
|
||||
main.create_trade(0.001, 5)
|
||||
(error, result) = rpc.rpc_trade_status()
|
||||
assert not error
|
||||
trade = result[0]
|
||||
@ -101,7 +101,7 @@ def test_rpc_trade_status(default_conf, update, ticker, mocker):
|
||||
|
||||
def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
_CONF=default_conf,
|
||||
@ -118,7 +118,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
|
||||
# Create some test data
|
||||
main.create_trade(0.001)
|
||||
main.create_trade(0.001, 5)
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
@ -154,7 +154,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
|
||||
def test_rpc_trade_statistics(
|
||||
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
_CONF=default_conf,
|
||||
@ -176,7 +176,7 @@ def test_rpc_trade_statistics(
|
||||
assert stats.find('no closed trade') >= 0
|
||||
|
||||
# Create some test data
|
||||
main.create_trade(0.001)
|
||||
main.create_trade(0.001, 5)
|
||||
trade = Trade.query.first()
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
@ -203,3 +203,32 @@ def test_rpc_trade_statistics(
|
||||
assert stats['avg_duration'] == '0:00:00'
|
||||
assert stats['best_pair'] == 'BTC_ETH'
|
||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||
|
||||
|
||||
def test_rpc_balance_handle(default_conf, update, mocker):
|
||||
mock_balance = [{
|
||||
'Currency': 'BTC',
|
||||
'Balance': 10.0,
|
||||
'Available': 12.0,
|
||||
'Pending': 0.0,
|
||||
'CryptoAddress': 'XXXX',
|
||||
}, {
|
||||
'Currency': 'ETH',
|
||||
'Balance': 0.0,
|
||||
'Available': 0.0,
|
||||
'Pending': 0.0,
|
||||
'CryptoAddress': 'XXXX',
|
||||
}]
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
get_balances=MagicMock(return_value=mock_balance))
|
||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||
|
||||
res = rpc.rpc_balance(default_conf['fiat_display_currency'])
|
||||
assert res
|
||||
# FIX: check returned result
|
||||
# res::
|
||||
# (False, ([{'currency': 'BTC', 'available': 12.0, 'balance': 10.0,
|
||||
# 'pending': 0.0, 'est_btc': 10.0}], 10.0, 'USD', 150000.0))
|
||||
|
@ -77,7 +77,7 @@ def test_authorized_only_exception(default_conf, mocker):
|
||||
|
||||
def test_status_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
@ -102,7 +102,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
# Trigger status while we have a fulfilled order for the open trade
|
||||
_status(bot=MagicMock(), update=update)
|
||||
|
||||
@ -112,7 +112,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
|
||||
|
||||
def test_status_table_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
@ -138,7 +138,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Create some test data
|
||||
create_trade(15.0)
|
||||
create_trade(15.0, int(default_conf['ticker_interval']))
|
||||
|
||||
_status_table(bot=MagicMock(), update=update)
|
||||
|
||||
@ -154,7 +154,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
|
||||
def test_profit_handle(
|
||||
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
@ -176,7 +176,7 @@ def test_profit_handle(
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
trade = Trade.query.first()
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
@ -210,7 +210,7 @@ def test_profit_handle(
|
||||
|
||||
def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
_CONF=default_conf,
|
||||
@ -219,13 +219,11 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
@ -239,7 +237,9 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker):
|
||||
_forcesell(bot=MagicMock(), update=update)
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||
@ -247,7 +247,7 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker):
|
||||
|
||||
def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
_CONF=default_conf,
|
||||
@ -256,13 +256,11 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
# Decrease the price and sell it
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
@ -276,7 +274,9 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
|
||||
_forcesell(bot=MagicMock(), update=update)
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||
@ -308,7 +308,7 @@ def test_exec_forcesell_open_orders(default_conf, ticker, mocker):
|
||||
|
||||
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
_CONF=default_conf,
|
||||
@ -317,14 +317,12 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
for _ in range(4):
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
rpc_mock.reset_mock()
|
||||
|
||||
update.message.text = '/forcesell all'
|
||||
@ -339,7 +337,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
||||
|
||||
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, True))
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
_CONF=default_conf,
|
||||
@ -376,7 +374,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||
def test_performance_handle(
|
||||
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
@ -389,7 +387,7 @@ def test_performance_handle(
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
@ -410,7 +408,7 @@ def test_performance_handle(
|
||||
def test_daily_handle(
|
||||
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
@ -427,7 +425,7 @@ def test_daily_handle(
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
@ -448,6 +446,28 @@ def test_daily_handle(
|
||||
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Reset msg_mock
|
||||
msg_mock.reset_mock()
|
||||
# Add two other trades
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trades = Trade.query.all()
|
||||
for trade in trades:
|
||||
trade.update(limit_buy_order)
|
||||
trade.update(limit_sell_order)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
update.message.text = '/daily 1'
|
||||
|
||||
_daily(bot=MagicMock(), update=update)
|
||||
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Try invalid data
|
||||
msg_mock.reset_mock()
|
||||
@ -460,7 +480,7 @@ def test_daily_handle(
|
||||
|
||||
def test_count_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram',
|
||||
@ -480,7 +500,7 @@ def test_count_handle(default_conf, update, ticker, mocker):
|
||||
update_state(State.RUNNING)
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
msg_mock.reset_mock()
|
||||
_count(bot=MagicMock(), update=update)
|
||||
|
||||
@ -492,7 +512,7 @@ def test_count_handle(default_conf, update, ticker, mocker):
|
||||
|
||||
def test_performance_handle_invalid(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, True))
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
_CONF=default_conf,
|
||||
@ -584,7 +604,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker):
|
||||
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_balance_handle(default_conf, update, mocker):
|
||||
def test_telegram_balance_handle(default_conf, update, mocker):
|
||||
mock_balance = [{
|
||||
'Currency': 'BTC',
|
||||
'Balance': 10.0,
|
||||
|
36
freqtrade/tests/strategy/test_default_strategy.py
Normal file
36
freqtrade/tests/strategy/test_default_strategy.py
Normal file
@ -0,0 +1,36 @@
|
||||
import json
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy, class_name
|
||||
from freqtrade.analyze import parse_ticker_dataframe
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def result():
|
||||
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||
return parse_ticker_dataframe(json.load(data_file))
|
||||
|
||||
|
||||
def test_default_strategy_class_name():
|
||||
assert class_name == DefaultStrategy.__name__
|
||||
|
||||
|
||||
def test_default_strategy_structure():
|
||||
assert hasattr(DefaultStrategy, 'minimal_roi')
|
||||
assert hasattr(DefaultStrategy, 'stoploss')
|
||||
assert hasattr(DefaultStrategy, 'ticker_interval')
|
||||
assert hasattr(DefaultStrategy, 'populate_indicators')
|
||||
assert hasattr(DefaultStrategy, 'populate_buy_trend')
|
||||
assert hasattr(DefaultStrategy, 'populate_sell_trend')
|
||||
|
||||
|
||||
def test_default_strategy(result):
|
||||
strategy = DefaultStrategy()
|
||||
|
||||
assert type(strategy.minimal_roi) is dict
|
||||
assert type(strategy.stoploss) is float
|
||||
assert type(strategy.ticker_interval) is int
|
||||
indicators = strategy.populate_indicators(result)
|
||||
assert type(indicators) is DataFrame
|
||||
assert type(strategy.populate_buy_trend(indicators)) is DataFrame
|
||||
assert type(strategy.populate_sell_trend(indicators)) is DataFrame
|
141
freqtrade/tests/strategy/test_strategy.py
Normal file
141
freqtrade/tests/strategy/test_strategy.py
Normal file
@ -0,0 +1,141 @@
|
||||
import json
|
||||
import logging
|
||||
import pytest
|
||||
from freqtrade.strategy.strategy import Strategy
|
||||
from freqtrade.analyze import parse_ticker_dataframe
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def result():
|
||||
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||
return parse_ticker_dataframe(json.load(data_file))
|
||||
|
||||
|
||||
def test_sanitize_module_name():
|
||||
assert Strategy._sanitize_module_name('default_strategy') == 'default_strategy'
|
||||
assert Strategy._sanitize_module_name('default_strategy.py') == 'default_strategy'
|
||||
assert Strategy._sanitize_module_name('../default_strategy.py') == 'default_strategy'
|
||||
assert Strategy._sanitize_module_name('../default_strategy') == 'default_strategy'
|
||||
assert Strategy._sanitize_module_name('.default_strategy') == '.default_strategy'
|
||||
assert Strategy._sanitize_module_name('foo-bar') == 'foo-bar'
|
||||
assert Strategy._sanitize_module_name('foo/bar') == 'bar'
|
||||
|
||||
|
||||
def test_search_strategy():
|
||||
assert Strategy._search_strategy('default_strategy') == '.'
|
||||
assert Strategy._search_strategy('super_duper') is None
|
||||
|
||||
|
||||
def test_strategy_structure():
|
||||
assert hasattr(Strategy, 'init')
|
||||
assert hasattr(Strategy, 'minimal_roi')
|
||||
assert hasattr(Strategy, 'stoploss')
|
||||
assert hasattr(Strategy, 'populate_indicators')
|
||||
assert hasattr(Strategy, 'populate_buy_trend')
|
||||
assert hasattr(Strategy, 'populate_sell_trend')
|
||||
|
||||
|
||||
def test_load_strategy(result):
|
||||
strategy = Strategy()
|
||||
strategy.logger = logging.getLogger(__name__)
|
||||
|
||||
assert not hasattr(Strategy, 'custom_strategy')
|
||||
strategy._load_strategy('default_strategy')
|
||||
|
||||
assert not hasattr(Strategy, 'custom_strategy')
|
||||
|
||||
assert hasattr(strategy.custom_strategy, 'populate_indicators')
|
||||
assert 'adx' in strategy.populate_indicators(result)
|
||||
|
||||
|
||||
def test_strategy(result):
|
||||
strategy = Strategy()
|
||||
strategy.init({'strategy': 'default_strategy'})
|
||||
|
||||
assert hasattr(strategy.custom_strategy, 'minimal_roi')
|
||||
assert strategy.minimal_roi['0'] == 0.04
|
||||
|
||||
assert hasattr(strategy.custom_strategy, 'stoploss')
|
||||
assert strategy.stoploss == -0.10
|
||||
|
||||
assert hasattr(strategy.custom_strategy, 'populate_indicators')
|
||||
assert 'adx' in strategy.populate_indicators(result)
|
||||
|
||||
assert hasattr(strategy.custom_strategy, 'populate_buy_trend')
|
||||
dataframe = strategy.populate_buy_trend(strategy.populate_indicators(result))
|
||||
assert 'buy' in dataframe.columns
|
||||
|
||||
assert hasattr(strategy.custom_strategy, 'populate_sell_trend')
|
||||
dataframe = strategy.populate_sell_trend(strategy.populate_indicators(result))
|
||||
assert 'sell' in dataframe.columns
|
||||
|
||||
|
||||
def test_strategy_override_minimal_roi(caplog):
|
||||
config = {
|
||||
'strategy': 'default_strategy',
|
||||
'minimal_roi': {
|
||||
"0": 0.5
|
||||
}
|
||||
}
|
||||
strategy = Strategy()
|
||||
strategy.init(config)
|
||||
|
||||
assert hasattr(strategy.custom_strategy, 'minimal_roi')
|
||||
assert strategy.minimal_roi['0'] == 0.5
|
||||
assert ('freqtrade.strategy.strategy',
|
||||
logging.INFO,
|
||||
'Override strategy \'minimal_roi\' with value in config file.'
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
def test_strategy_override_stoploss(caplog):
|
||||
config = {
|
||||
'strategy': 'default_strategy',
|
||||
'stoploss': -0.5
|
||||
}
|
||||
strategy = Strategy()
|
||||
strategy.init(config)
|
||||
|
||||
assert hasattr(strategy.custom_strategy, 'stoploss')
|
||||
assert strategy.stoploss == -0.5
|
||||
assert ('freqtrade.strategy.strategy',
|
||||
logging.INFO,
|
||||
'Override strategy \'stoploss\' with value in config file: -0.5.'
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
def test_strategy_override_ticker_interval(caplog):
|
||||
config = {
|
||||
'strategy': 'default_strategy',
|
||||
'ticker_interval': 60
|
||||
}
|
||||
strategy = Strategy()
|
||||
strategy.init(config)
|
||||
|
||||
assert hasattr(strategy.custom_strategy, 'ticker_interval')
|
||||
assert strategy.ticker_interval == 60
|
||||
assert ('freqtrade.strategy.strategy',
|
||||
logging.INFO,
|
||||
'Override strategy \'ticker_interval\' with value in config file: 60.'
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
def test_strategy_fallback_default_strategy():
|
||||
strategy = Strategy()
|
||||
strategy.logger = logging.getLogger(__name__)
|
||||
|
||||
assert not hasattr(Strategy, 'custom_strategy')
|
||||
strategy._load_strategy('../../super_duper')
|
||||
assert not hasattr(Strategy, 'custom_strategy')
|
||||
|
||||
|
||||
def test_strategy_singleton():
|
||||
strategy1 = Strategy()
|
||||
strategy1.init({'strategy': 'default_strategy'})
|
||||
|
||||
assert hasattr(strategy1.custom_strategy, 'minimal_roi')
|
||||
assert strategy1.minimal_roi['0'] == 0.04
|
||||
|
||||
strategy2 = Strategy()
|
||||
assert hasattr(strategy2.custom_strategy, 'minimal_roi')
|
||||
assert strategy2.minimal_roi['0'] == 0.04
|
@ -1,14 +1,17 @@
|
||||
# pragma pylint: disable=missing-docstring,W0621
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
import freqtrade.tests.conftest as tt # test tools
|
||||
|
||||
import arrow
|
||||
import datetime
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.analyze import (SignalType, get_signal, parse_ticker_dataframe,
|
||||
from freqtrade.analyze import (get_signal, parse_ticker_dataframe,
|
||||
populate_buy_trend, populate_indicators,
|
||||
populate_sell_trend)
|
||||
from freqtrade.strategy.strategy import Strategy
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -27,11 +30,17 @@ def test_dataframe_correct_length(result):
|
||||
|
||||
|
||||
def test_populates_buy_trend(result):
|
||||
# Load the default strategy for the unit test, because this logic is done in main.py
|
||||
Strategy().init({'strategy': 'default_strategy'})
|
||||
|
||||
dataframe = populate_buy_trend(populate_indicators(result))
|
||||
assert 'buy' in dataframe.columns
|
||||
|
||||
|
||||
def test_populates_sell_trend(result):
|
||||
# Load the default strategy for the unit test, because this logic is done in main.py
|
||||
Strategy().init({'strategy': 'default_strategy'})
|
||||
|
||||
dataframe = populate_sell_trend(populate_indicators(result))
|
||||
assert 'sell' in dataframe.columns
|
||||
|
||||
@ -40,30 +49,65 @@ def test_returns_latest_buy_signal(mocker):
|
||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.analyze.analyze_ticker',
|
||||
return_value=DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
||||
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert get_signal('BTC-ETH', SignalType.BUY)
|
||||
assert get_signal('BTC-ETH', 5) == (True, False)
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.analyze.analyze_ticker',
|
||||
return_value=DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
||||
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert not get_signal('BTC-ETH', SignalType.BUY)
|
||||
assert get_signal('BTC-ETH', 5) == (False, True)
|
||||
|
||||
|
||||
def test_returns_latest_sell_signal(mocker):
|
||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.analyze.analyze_ticker',
|
||||
return_value=DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
|
||||
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert get_signal('BTC-ETH', SignalType.SELL)
|
||||
assert get_signal('BTC-ETH', 5) == (False, True)
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.analyze.analyze_ticker',
|
||||
return_value=DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
|
||||
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert not get_signal('BTC-ETH', SignalType.SELL)
|
||||
assert get_signal('BTC-ETH', 5) == (True, False)
|
||||
|
||||
|
||||
def test_get_signal_empty(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None)
|
||||
assert (False, False) == get_signal('foo', int(default_conf['ticker_interval']))
|
||||
assert tt.log_has('Empty ticker history for pair foo',
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker',
|
||||
side_effect=ValueError('xyz'))
|
||||
assert (False, False) == get_signal('foo', int(default_conf['ticker_interval']))
|
||||
assert tt.log_has('Unable to analyze ticker for pair foo: xyz',
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame([]))
|
||||
assert (False, False) == get_signal('xyz', int(default_conf['ticker_interval']))
|
||||
assert tt.log_has('Empty dataframe for pair xyz',
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_signal_old_dataframe(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
||||
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
|
||||
oldtime = arrow.utcnow() - datetime.timedelta(minutes=11)
|
||||
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame(ticks))
|
||||
assert (False, False) == get_signal('xyz', int(default_conf['ticker_interval']))
|
||||
assert tt.log_has('Too old dataframe for pair xyz',
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_signal_handles_exceptions(mocker):
|
||||
@ -71,4 +115,17 @@ def test_get_signal_handles_exceptions(mocker):
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker',
|
||||
side_effect=Exception('invalid ticker history '))
|
||||
|
||||
assert not get_signal('BTC-ETH', SignalType.BUY)
|
||||
assert get_signal('BTC-ETH', 5) == (False, False)
|
||||
|
||||
|
||||
def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv):
|
||||
|
||||
columns = ['close', 'high', 'low', 'open', 'date', 'volume']
|
||||
|
||||
# Test file with BV data
|
||||
dataframe = parse_ticker_dataframe(ticker_history)
|
||||
assert dataframe.columns.tolist() == columns
|
||||
|
||||
# Test file without BV data
|
||||
dataframe = parse_ticker_dataframe(ticker_history_without_bv)
|
||||
assert dataframe.columns.tolist() == columns
|
||||
|
@ -116,9 +116,9 @@ def test_fiat_convert_get_price(mocker):
|
||||
assert fiat_convert._pairs[0]._expiration is not expiration
|
||||
|
||||
|
||||
def test_fiat_convert_without_network(mocker):
|
||||
pymarketcap = MagicMock(side_effect=ImportError('Oh boy, you have no network!'))
|
||||
mocker.patch('freqtrade.fiat_convert.Pymarketcap', pymarketcap)
|
||||
def test_fiat_convert_without_network():
|
||||
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
|
||||
CryptoToFiatConverter._coinmarketcap = None
|
||||
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
assert fiat_convert._coinmarketcap is None
|
||||
|
@ -2,6 +2,7 @@
|
||||
import copy
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
import freqtrade.tests.conftest as tt # test tools
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
@ -10,7 +11,6 @@ from sqlalchemy import create_engine
|
||||
|
||||
import freqtrade.main as main
|
||||
from freqtrade import DependencyException, OperationalException
|
||||
from freqtrade.analyze import SignalType
|
||||
from freqtrade.exchange import Exchanges
|
||||
from freqtrade.main import (_process, check_handle_timedout, create_trade,
|
||||
execute_sell, get_target_bid, handle_trade, init)
|
||||
@ -21,11 +21,10 @@ from freqtrade.persistence import Trade
|
||||
def test_parse_args_backtesting(mocker):
|
||||
""" Test that main() can start backtesting or hyperopt.
|
||||
and also ensure we can pass some specific arguments
|
||||
argument parsing is done in test_misc.py """
|
||||
further argument parsing is done in test_misc.py """
|
||||
backtesting_mock = mocker.patch(
|
||||
'freqtrade.optimize.backtesting.start', MagicMock())
|
||||
with pytest.raises(SystemExit, match=r'0'):
|
||||
main.main(['backtesting'])
|
||||
main.main(['backtesting'])
|
||||
assert backtesting_mock.call_count == 1
|
||||
call_args = backtesting_mock.call_args[0][0]
|
||||
assert call_args.config == 'config.json'
|
||||
@ -39,8 +38,7 @@ def test_parse_args_backtesting(mocker):
|
||||
def test_main_start_hyperopt(mocker):
|
||||
hyperopt_mock = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.start', MagicMock())
|
||||
with pytest.raises(SystemExit, match=r'0'):
|
||||
main.main(['hyperopt'])
|
||||
main.main(['hyperopt'])
|
||||
assert hyperopt_mock.call_count == 1
|
||||
call_args = hyperopt_mock.call_args[0][0]
|
||||
assert call_args.config == 'config.json'
|
||||
@ -49,10 +47,38 @@ def test_main_start_hyperopt(mocker):
|
||||
assert call_args.func is not None
|
||||
|
||||
|
||||
def test_process_maybe_execute_buy(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.create_trade', return_value=True)
|
||||
assert main.process_maybe_execute_buy(default_conf, int(default_conf['ticker_interval']))
|
||||
mocker.patch('freqtrade.main.create_trade', return_value=False)
|
||||
assert not main.process_maybe_execute_buy(default_conf, int(default_conf['ticker_interval']))
|
||||
|
||||
|
||||
def test_process_maybe_execute_sell(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.handle_trade', return_value=True)
|
||||
mocker.patch('freqtrade.exchange.get_order', return_value=1)
|
||||
trade = MagicMock()
|
||||
trade.open_order_id = '123'
|
||||
assert not main.process_maybe_execute_sell(trade, int(default_conf['ticker_interval']))
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
# Assert we call handle_trade() if trade is feasible for execution
|
||||
assert main.process_maybe_execute_sell(trade, int(default_conf['ticker_interval']))
|
||||
|
||||
|
||||
def test_process_maybe_execute_buy_exception(default_conf, mocker, caplog):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.create_trade', MagicMock(side_effect=DependencyException))
|
||||
main.process_maybe_execute_buy(default_conf, int(default_conf['ticker_interval']))
|
||||
tt.log_has('Unable to create trade:', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
@ -64,7 +90,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, m
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert not trades
|
||||
|
||||
result = _process()
|
||||
result = _process(interval=int(default_conf['ticker_interval']))
|
||||
assert result is True
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
@ -82,7 +108,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, m
|
||||
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -90,7 +116,7 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
||||
get_wallet_health=health,
|
||||
buy=MagicMock(side_effect=requests.exceptions.RequestException))
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
result = _process()
|
||||
result = _process(interval=int(default_conf['ticker_interval']))
|
||||
assert result is False
|
||||
assert sleep_mock.has_calls()
|
||||
|
||||
@ -99,7 +125,7 @@ def test_process_operational_exception(default_conf, ticker, health, mocker):
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
@ -108,7 +134,7 @@ def test_process_operational_exception(default_conf, ticker, health, mocker):
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
assert get_state() == State.RUNNING
|
||||
|
||||
result = _process()
|
||||
result = _process(interval=int(default_conf['ticker_interval']))
|
||||
assert result is False
|
||||
assert get_state() == State.STOPPED
|
||||
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
|
||||
@ -117,8 +143,7 @@ def test_process_operational_exception(default_conf, ticker, health, mocker):
|
||||
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_signal',
|
||||
side_effect=lambda *args: False if args[1] == SignalType.SELL else True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
@ -129,18 +154,18 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert not trades
|
||||
result = _process()
|
||||
result = _process(interval=int(default_conf['ticker_interval']))
|
||||
assert result is True
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert len(trades) == 1
|
||||
|
||||
result = _process()
|
||||
result = _process(interval=int(default_conf['ticker_interval']))
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -150,7 +175,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade is not None
|
||||
@ -171,7 +196,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||
def test_create_trade_minimal_amount(default_conf, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
buy_mock = mocker.patch(
|
||||
'freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy')
|
||||
)
|
||||
@ -180,14 +205,14 @@ def test_create_trade_minimal_amount(default_conf, ticker, mocker):
|
||||
get_ticker=ticker)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
min_stake_amount = 0.0005
|
||||
create_trade(min_stake_amount)
|
||||
create_trade(min_stake_amount, int(default_conf['ticker_interval']))
|
||||
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
|
||||
assert rate * amount >= min_stake_amount
|
||||
|
||||
|
||||
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -195,12 +220,12 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
|
||||
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
||||
create_trade(default_conf['stake_amount'])
|
||||
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
|
||||
|
||||
|
||||
def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -211,12 +236,12 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
||||
conf = copy.deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = []
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
create_trade(default_conf['stake_amount'])
|
||||
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
|
||||
|
||||
|
||||
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -228,12 +253,26 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
|
||||
conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
|
||||
conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
create_trade(default_conf['stake_amount'])
|
||||
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
|
||||
|
||||
|
||||
def test_create_trade_no_signal(default_conf, ticker, mocker):
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', MagicMock(return_value=(False, False)))
|
||||
mocker.patch.multiple('freqtrade.exchange',
|
||||
get_ticker_history=MagicMock(return_value=20))
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
get_balance=MagicMock(return_value=20))
|
||||
stake_amount = 10
|
||||
Trade.query = MagicMock()
|
||||
Trade.query.filter = MagicMock()
|
||||
assert not create_trade(stake_amount, int(default_conf['ticker_interval']))
|
||||
|
||||
|
||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -248,7 +287,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
@ -256,7 +295,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||
trade.update(limit_buy_order)
|
||||
assert trade.is_open is True
|
||||
|
||||
handle_trade(trade)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
||||
assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
|
||||
assert trade.open_order_id == 'mocked_limit_sell'
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
@ -268,11 +308,57 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||
assert trade.close_date is not None
|
||||
|
||||
|
||||
def test_handle_overlpapping_signals(default_conf, ticker, mocker, caplog):
|
||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, True))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
# Buy and Sell triggering, so doing nothing ...
|
||||
trades = Trade.query.all()
|
||||
assert len(trades) == 0
|
||||
|
||||
# Buy is triggering, so buying ...
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
trades = Trade.query.all()
|
||||
assert len(trades) == 1
|
||||
assert trades[0].is_open is True
|
||||
|
||||
# Buy and Sell are not triggering, so doing nothing ...
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, False))
|
||||
assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is False
|
||||
trades = Trade.query.all()
|
||||
assert len(trades) == 1
|
||||
assert trades[0].is_open is True
|
||||
|
||||
# Buy and Sell are triggering, so doing nothing ...
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, True))
|
||||
assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is False
|
||||
trades = Trade.query.all()
|
||||
assert len(trades) == 1
|
||||
assert trades[0].is_open is True
|
||||
|
||||
# Sell is triggering, guess what : we are Selling!
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
||||
trades = Trade.query.all()
|
||||
assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is True
|
||||
|
||||
|
||||
def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
|
||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -281,7 +367,7 @@ def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=True)
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade.is_open = True
|
||||
@ -291,12 +377,12 @@ def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
|
||||
# we might just want to check if we are in a sell condition without
|
||||
# executing
|
||||
# if ROI is reached we must sell
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
|
||||
assert handle_trade(trade)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
||||
assert handle_trade(trade, interval=int(default_conf['ticker_interval']))
|
||||
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
|
||||
# if ROI is reached we must sell even if sell-signal is not signalled
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
assert handle_trade(trade)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
||||
assert handle_trade(trade, interval=int(default_conf['ticker_interval']))
|
||||
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
|
||||
|
||||
|
||||
@ -304,7 +390,7 @@ def test_handle_trade_experimental(default_conf, ticker, mocker, caplog):
|
||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -313,24 +399,23 @@ def test_handle_trade_experimental(default_conf, ticker, mocker, caplog):
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade.is_open = True
|
||||
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
|
||||
value_returned = handle_trade(trade)
|
||||
assert ('freqtrade', logging.DEBUG, 'Checking sell_signal ...') in caplog.record_tuples
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, False))
|
||||
value_returned = handle_trade(trade, int(default_conf['ticker_interval']))
|
||||
assert value_returned is False
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
assert handle_trade(trade)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
||||
assert handle_trade(trade, int(default_conf['ticker_interval']))
|
||||
s = 'Executing sell due to sell signal ...'
|
||||
assert ('freqtrade', logging.DEBUG, s) in caplog.record_tuples
|
||||
|
||||
|
||||
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -339,7 +424,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
|
||||
|
||||
# Create trade and sell it
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
@ -349,13 +434,14 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
|
||||
assert trade.is_open is False
|
||||
|
||||
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
||||
handle_trade(trade)
|
||||
handle_trade(trade, int(default_conf['ticker_interval']))
|
||||
|
||||
|
||||
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
@ -380,14 +466,30 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mo
|
||||
# check it does cancel buy orders over the time limit
|
||||
check_handle_timedout(600)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
||||
assert len(trades) == 0
|
||||
|
||||
|
||||
def test_handle_timedout_limit_buy(default_conf, mocker):
|
||||
cancel_order = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.cancel_order', cancel_order)
|
||||
Trade.session = MagicMock()
|
||||
trade = MagicMock()
|
||||
order = {'remaining': 1,
|
||||
'amount': 1}
|
||||
assert main.handle_timedout_limit_buy(trade, order)
|
||||
assert cancel_order.call_count == 1
|
||||
order['amount'] = 2
|
||||
assert not main.handle_timedout_limit_buy(trade, order)
|
||||
assert cancel_order.call_count == 2
|
||||
|
||||
|
||||
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
@ -413,14 +515,30 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
|
||||
# check it does cancel sell orders over the time limit
|
||||
check_handle_timedout(600)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
assert trade_sell.is_open is True
|
||||
|
||||
|
||||
def test_handle_timedout_limit_sell(default_conf, mocker):
|
||||
cancel_order = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.cancel_order', cancel_order)
|
||||
trade = MagicMock()
|
||||
order = {'remaining': 1,
|
||||
'amount': 1}
|
||||
assert main.handle_timedout_limit_sell(trade, order)
|
||||
assert cancel_order.call_count == 1
|
||||
order['amount'] = 2
|
||||
assert not main.handle_timedout_limit_sell(trade, order)
|
||||
# Assert cancel_order was not called (callcount remains unchanged)
|
||||
assert cancel_order.call_count == 1
|
||||
|
||||
|
||||
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
|
||||
mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
@ -446,6 +564,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
|
||||
# note this is for a partially-complete buy order
|
||||
check_handle_timedout(600)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
||||
assert len(trades) == 1
|
||||
assert trades[0].amount == 23.0
|
||||
@ -469,19 +588,17 @@ def test_balance_bigger_last_ask(mocker):
|
||||
|
||||
def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
@ -494,7 +611,10 @@ def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
|
||||
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Profit' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||
@ -502,7 +622,7 @@ def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
|
||||
|
||||
def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
@ -512,13 +632,11 @@ def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
@ -531,15 +649,17 @@ def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
|
||||
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker):
|
||||
def test_execute_sell_without_conf_sell_down(default_conf, ticker, ticker_sell_down, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
@ -548,7 +668,38 @@ def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Decrease the price and sell it
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker_sell_down)
|
||||
mocker.patch('freqtrade.main._CONF', {})
|
||||
|
||||
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_execute_sell_without_conf_sell_up(default_conf, ticker, ticker_sell_up, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
@ -562,7 +713,9 @@ def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker)
|
||||
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'USD' not in rpc_mock.call_args_list[-1][0][0]
|
||||
@ -576,7 +729,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
|
||||
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -588,11 +741,12 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade.update(limit_buy_order)
|
||||
assert handle_trade(trade) is True
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
||||
assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
|
||||
|
||||
|
||||
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
|
||||
@ -603,7 +757,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
|
||||
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -615,11 +769,12 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade.update(limit_buy_order)
|
||||
assert handle_trade(trade) is True
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
||||
assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
|
||||
|
||||
|
||||
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
|
||||
@ -630,7 +785,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
|
||||
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -642,11 +797,12 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade.update(limit_buy_order)
|
||||
assert handle_trade(trade) is False
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
||||
assert handle_trade(trade, int(default_conf['ticker_interval'])) is False
|
||||
|
||||
|
||||
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
|
||||
@ -657,7 +813,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
|
||||
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
@ -669,8 +825,9 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
create_trade(0.001, int(default_conf['ticker_interval']))
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade.update(limit_buy_order)
|
||||
assert handle_trade(trade) is True
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, True))
|
||||
assert handle_trade(trade, int(default_conf['ticker_interval'])) is True
|
||||
|
@ -5,10 +5,11 @@ import time
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from jsonschema import ValidationError
|
||||
|
||||
from freqtrade.misc import (common_args_parser, load_config, parse_args,
|
||||
throttle)
|
||||
throttle, file_dump_json, parse_timerange)
|
||||
|
||||
|
||||
def test_throttle():
|
||||
@ -133,6 +134,21 @@ def test_parse_args_hyperopt_custom(mocker):
|
||||
assert call_args.func is not None
|
||||
|
||||
|
||||
def test_file_dump_json(default_conf, mocker):
|
||||
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
||||
json_dump = mocker.patch('json.dump', MagicMock())
|
||||
file_dump_json('somefile', [1, 2, 3])
|
||||
assert file_open.call_count == 1
|
||||
assert json_dump.call_count == 1
|
||||
|
||||
|
||||
def test_parse_timerange_incorrect():
|
||||
assert ((None, 'line'), None, -200) == parse_timerange('-200')
|
||||
assert (('line', None), 200, None) == parse_timerange('200-')
|
||||
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
||||
parse_timerange('-')
|
||||
|
||||
|
||||
def test_load_config(default_conf, mocker):
|
||||
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
|
@ -1,10 +1,9 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from freqtrade.exchange import Exchanges
|
||||
from freqtrade.persistence import Trade, init
|
||||
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
||||
|
||||
|
||||
def test_init_create_session(default_conf, mocker):
|
||||
@ -310,3 +309,50 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order):
|
||||
|
||||
# Test with a custom fee rate on the close trade
|
||||
assert trade.calc_profit_percent(fee=0.003) == 0.0614782
|
||||
|
||||
|
||||
def test_clean_dry_run_db(default_conf):
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
pair='BTC_ETH',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee=0.0025,
|
||||
open_rate=0.123,
|
||||
exchange='BITTREX',
|
||||
open_order_id='dry_run_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
trade = Trade(
|
||||
pair='BTC_ETC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee=0.0025,
|
||||
open_rate=0.123,
|
||||
exchange='BITTREX',
|
||||
open_order_id='dry_run_sell_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
# Simulate prod entry
|
||||
trade = Trade(
|
||||
pair='BTC_ETC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee=0.0025,
|
||||
open_rate=0.123,
|
||||
exchange='BITTREX',
|
||||
open_order_id='prod_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
# We have 3 entries: 2 dry_run, 1 prod
|
||||
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 3
|
||||
|
||||
clean_dry_run_db()
|
||||
|
||||
# We have now only the prod
|
||||
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
|
||||
|
1
freqtrade/tests/testdata/BTC_UNITEST-30.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_UNITEST-30.json
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
||||
python-bittrex==0.2.2
|
||||
SQLAlchemy==1.2.1
|
||||
SQLAlchemy==1.2.2
|
||||
python-telegram-bot==9.0.0
|
||||
arrow==0.12.0
|
||||
arrow==0.12.1
|
||||
cachetools==2.0.1
|
||||
requests==2.18.4
|
||||
urllib3==1.22
|
||||
@ -11,7 +11,7 @@ scikit-learn==0.19.1
|
||||
scipy==1.0.0
|
||||
jsonschema==2.6.0
|
||||
numpy==1.14.0
|
||||
TA-Lib==0.4.15
|
||||
TA-Lib==0.4.16
|
||||
pytest==3.3.2
|
||||
pytest-mock==1.6.3
|
||||
pytest-cov==2.5.1
|
||||
@ -19,7 +19,7 @@ hyperopt==0.1
|
||||
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
||||
networkx==1.11
|
||||
tabulate==0.8.2
|
||||
pymarketcap==3.3.148
|
||||
pymarketcap==3.3.150
|
||||
|
||||
# Required for plotting data
|
||||
#matplotlib==2.1.0
|
||||
|
@ -1,22 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import matplotlib # Install PYQT5 manually if you want to test this helper function
|
||||
matplotlib.use("Qt5Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import talib.abstract as ta
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from pandas import DataFrame
|
||||
from freqtrade import exchange, analyze
|
||||
from freqtrade.misc import common_args_parser
|
||||
from freqtrade.strategy.strategy import Strategy
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib # Install PYQT5 manually if you want to test this helper function
|
||||
|
||||
matplotlib.use("Qt5Agg")
|
||||
|
||||
|
||||
def plot_parse_args(args ):
|
||||
def plot_parse_args(args):
|
||||
parser = common_args_parser(description='Graph utility')
|
||||
parser.add_argument(
|
||||
'-p', '--pair',
|
||||
help = 'What currency pair',
|
||||
dest = 'pair',
|
||||
default = 'BTC_ETH',
|
||||
type = str,
|
||||
help='What currency pair',
|
||||
dest='pair',
|
||||
default='BTC_ETH',
|
||||
type=str,
|
||||
)
|
||||
parser.add_argument(
|
||||
'-i', '--interval',
|
||||
help='what interval to use',
|
||||
dest='interval',
|
||||
default=5,
|
||||
type=int,
|
||||
)
|
||||
return parser.parse_args(args)
|
||||
|
||||
@ -27,24 +38,25 @@ def plot_analyzed_dataframe(args) -> None:
|
||||
:param pair: pair as str
|
||||
:return: None
|
||||
"""
|
||||
pair = args.pair
|
||||
# Init strategy
|
||||
strategy = Strategy()
|
||||
strategy.init({'strategy': args.strategy})
|
||||
|
||||
# Init Bittrex to use public API
|
||||
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
||||
ticker = exchange.get_ticker_history(pair)
|
||||
ticker = exchange.get_ticker_history(args.pair, args.interval)
|
||||
dataframe = analyze.analyze_ticker(ticker)
|
||||
|
||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
||||
dataframe = populate_indicator(dataframe)
|
||||
|
||||
# Two subplots sharing x axis
|
||||
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
||||
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
||||
fig.suptitle(args.pair + " " + str(args.interval), fontsize=14, fontweight='bold')
|
||||
ax1.plot(dataframe.index.values, dataframe['close'], label='close')
|
||||
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
|
||||
ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA')
|
||||
ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA')
|
||||
ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low')
|
||||
ax1.plot(dataframe.index.values, dataframe['bb_lowerband'], '-.', label='BB low')
|
||||
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
|
||||
ax1.legend()
|
||||
|
||||
@ -65,6 +77,41 @@ def plot_analyzed_dataframe(args) -> None:
|
||||
plt.show()
|
||||
|
||||
|
||||
def populate_indicator(dataframe: DataFrame) -> DataFrame:
|
||||
|
||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
||||
|
||||
# ADX
|
||||
if 'adx' not in dataframe:
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
|
||||
# Bollinger bands
|
||||
if 'bb_lowerband' not in dataframe:
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
|
||||
# Stoch fast
|
||||
if 'fastd' not in dataframe or 'fastk' not in dataframe:
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
|
||||
# MFI
|
||||
if 'mfi' not in dataframe:
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
|
||||
# SMA - Simple Moving Average
|
||||
if 'sma' not in dataframe:
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
if 'tema' not in dataframe:
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = plot_parse_args(sys.argv[1:])
|
||||
plot_analyzed_dataframe(args)
|
||||
|
160
scripts/plot_profit.py
Executable file
160
scripts/plot_profit.py
Executable file
@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import json
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
import freqtrade.optimize as optimize
|
||||
import freqtrade.misc as misc
|
||||
from freqtrade.strategy.strategy import Strategy
|
||||
|
||||
|
||||
def plot_parse_args(args):
|
||||
parser = misc.common_args_parser('Graph utility')
|
||||
# FIX: perhaps delete those backtesting options that are not feasible (shows up in -h)
|
||||
misc.backtesting_options(parser)
|
||||
parser.add_argument(
|
||||
'-p', '--pair',
|
||||
help='Show profits for only this pairs. Pairs are comma-separated.',
|
||||
dest='pair',
|
||||
default=None
|
||||
)
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
||||
# data:: [ pair, profit-%, enter, exit, time, duration]
|
||||
# data:: ['BTC_XMR', 0.00537847, '1511176800', '1511178000', 5057, 1]
|
||||
# FIX: make use of the enter/exit dates to insert the
|
||||
# profit more precisely into the pg array
|
||||
def make_profit_array(data, px, filter_pairs=[]):
|
||||
pg = np.zeros(px)
|
||||
# Go through the trades
|
||||
# and make an total profit
|
||||
# array
|
||||
for trade in data:
|
||||
pair = trade[0]
|
||||
if filter_pairs and pair not in filter_pairs:
|
||||
continue
|
||||
profit = trade[1]
|
||||
tim = trade[4]
|
||||
dur = trade[5]
|
||||
pg[tim+dur-1] += profit
|
||||
|
||||
# rewrite the pg array to go from
|
||||
# total profits at each timeframe
|
||||
# to accumulated profits
|
||||
pa = 0
|
||||
for x in range(0, len(pg)):
|
||||
p = pg[x] # Get current total percent
|
||||
pa += p # Add to the accumulated percent
|
||||
pg[x] = pa # write back to save memory
|
||||
|
||||
return pg
|
||||
|
||||
|
||||
def plot_profit(args) -> None:
|
||||
"""
|
||||
Plots the total profit for all pairs.
|
||||
Note, the profit calculation isn't realistic.
|
||||
But should be somewhat proportional, and therefor useful
|
||||
in helping out to find a good algorithm.
|
||||
"""
|
||||
|
||||
# We need to use the same pairs, same tick_interval
|
||||
# and same timeperiod as used in backtesting
|
||||
# to match the tickerdata against the profits-results
|
||||
|
||||
filter_pairs = args.pair
|
||||
|
||||
config = misc.load_config(args.config)
|
||||
config.update({'strategy': args.strategy})
|
||||
|
||||
# Init strategy
|
||||
strategy = Strategy()
|
||||
strategy.init(config)
|
||||
|
||||
pairs = config['exchange']['pair_whitelist']
|
||||
|
||||
if filter_pairs:
|
||||
filter_pairs = filter_pairs.split(',')
|
||||
pairs = list(set(pairs) & set(filter_pairs))
|
||||
print('Filter, keep pairs %s' % pairs)
|
||||
|
||||
tickers = optimize.load_data(args.datadir, pairs=pairs,
|
||||
ticker_interval=strategy.ticker_interval,
|
||||
refresh_pairs=False)
|
||||
dataframes = optimize.preprocess(tickers)
|
||||
|
||||
# Make an average close price of all the pairs that was involved.
|
||||
# this could be useful to gauge the overall market trend
|
||||
|
||||
# FIX: since the dataframes are of unequal length,
|
||||
# andor has different dates, we need to merge them
|
||||
# But we dont have the date information in the
|
||||
# backtesting results, this is needed to match the dates
|
||||
# For now, assume the dataframes are aligned.
|
||||
max_x = 0
|
||||
for pair, pair_data in dataframes.items():
|
||||
n = len(pair_data['close'])
|
||||
max_x = max(max_x, n)
|
||||
# if max_x != n:
|
||||
# raise Exception('Please rerun script. Input data has different lengths %s'
|
||||
# %('Different pair length: %s <=> %s' %(max_x, n)))
|
||||
print('max_x: %s' % (max_x))
|
||||
|
||||
# We are essentially saying:
|
||||
# array <- sum dataframes[*]['close'] / num_items dataframes
|
||||
# FIX: there should be some onliner numpy/panda for this
|
||||
avgclose = np.zeros(max_x)
|
||||
num = 0
|
||||
for pair, pair_data in dataframes.items():
|
||||
close = pair_data['close']
|
||||
maxprice = max(close) # Normalize price to [0,1]
|
||||
print('Pair %s has length %s' % (pair, len(close)))
|
||||
for x in range(0, len(close)):
|
||||
avgclose[x] += close[x] / maxprice
|
||||
# avgclose += close
|
||||
num += 1
|
||||
avgclose /= num
|
||||
|
||||
# Load the profits results
|
||||
# And make an profits-growth array
|
||||
|
||||
filename = 'backtest-result.json'
|
||||
with open(filename) as file:
|
||||
data = json.load(file)
|
||||
pg = make_profit_array(data, max_x, filter_pairs)
|
||||
|
||||
#
|
||||
# Plot the pairs average close prices, and total profit growth
|
||||
#
|
||||
|
||||
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
||||
fig.suptitle('total profit')
|
||||
ax1.plot(avgclose, label='avgclose')
|
||||
ax2.plot(pg, label='profit')
|
||||
ax1.legend(loc='upper left')
|
||||
ax2.legend(loc='upper left')
|
||||
|
||||
# FIX if we have one line pair in paris
|
||||
# then skip the plotting of the third graph,
|
||||
# or change what we plot
|
||||
# In third graph, we plot each profit separately
|
||||
for pair in pairs:
|
||||
pg = make_profit_array(data, max_x, pair)
|
||||
ax3.plot(pg, label=pair)
|
||||
ax3.legend(loc='upper left')
|
||||
# black background to easier see multiple colors
|
||||
ax3.set_facecolor('black')
|
||||
|
||||
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
||||
# all but bottom plot.
|
||||
fig.subplots_adjust(hspace=0)
|
||||
plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False)
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = plot_parse_args(sys.argv[1:])
|
||||
plot_profit(args)
|
0
user_data/data/.gitkeep
Normal file
0
user_data/data/.gitkeep
Normal file
0
user_data/strategies/__init__.py
Normal file
0
user_data/strategies/__init__.py
Normal file
246
user_data/strategies/test_strategy.py
Normal file
246
user_data/strategies/test_strategy.py
Normal file
@ -0,0 +1,246 @@
|
||||
|
||||
# --- Do not remove these libs ---
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from pandas import DataFrame
|
||||
# --------------------------------
|
||||
|
||||
# Add your lib to import here
|
||||
import talib.abstract as ta
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
import numpy # noqa
|
||||
|
||||
|
||||
# Update this variable if you change the class name
|
||||
class_name = 'TestStrategy'
|
||||
|
||||
|
||||
# This class is a sample. Feel free to customize it.
|
||||
class TestStrategy(IStrategy):
|
||||
"""
|
||||
This is a test strategy to inspire you.
|
||||
More information in https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md
|
||||
|
||||
You can:
|
||||
- Rename the class name (Do not forget to update class_name)
|
||||
- Add any methods you want to build your strategy
|
||||
- Add any lib you need to build your strategy
|
||||
|
||||
You must keep:
|
||||
- the lib in the section "Do not remove these libs"
|
||||
- the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend,
|
||||
populate_sell_trend, hyperopt_space, buy_strategy_generator
|
||||
"""
|
||||
|
||||
# Minimal ROI designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||
minimal_roi = {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
}
|
||||
|
||||
# Optimal stoploss designed for the strategy
|
||||
# This attribute will be overridden if the config file contains "stoploss"
|
||||
stoploss = -0.10
|
||||
|
||||
# Optimal ticker interval for the strategy
|
||||
ticker_interval = 5
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
# ------------------------------------
|
||||
|
||||
# ADX
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
|
||||
"""
|
||||
# Awesome oscillator
|
||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
|
||||
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
|
||||
# MFI
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
|
||||
# Minus Directional Indicator / Movement
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# Plus Directional Indicator / Movement
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# ROC
|
||||
dataframe['roc'] = ta.ROC(dataframe)
|
||||
|
||||
# RSI
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
|
||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||
|
||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
|
||||
# Stoch
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
|
||||
# Stoch RSI
|
||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
"""
|
||||
|
||||
# Overlap Studies
|
||||
# ------------------------------------
|
||||
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
|
||||
"""
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
|
||||
# SAR Parabol
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
|
||||
# SMA - Simple Moving Average
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
"""
|
||||
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
# Cycle Indicator
|
||||
# ------------------------------------
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hammer: values [0, 100]
|
||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
# Inverted Hammer: values [0, 100]
|
||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
# Dragonfly Doji: values [0, 100]
|
||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
# Piercing Line: values [0, 100]
|
||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
# Morningstar: values [0, 100]
|
||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
# Three White Soldiers: values [0, 100]
|
||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hanging Man: values [0, 100]
|
||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
# Shooting Star: values [0, 100]
|
||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
# Gravestone Doji: values [0, 100]
|
||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
# Dark Cloud Cover: values [0, 100]
|
||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
# Evening Doji Star: values [0, 100]
|
||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
# Evening Star: values [0, 100]
|
||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Three Line Strike: values [0, -100, 100]
|
||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
# Spinning Top: values [0, -100, 100]
|
||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
# Engulfing: values [0, -100, 100]
|
||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
# Harami: values [0, -100, 100]
|
||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
# Three Outside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
# Three Inside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
"""
|
||||
|
||||
# Chart type
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Heikinashi stategy
|
||||
heikinashi = qtpylib.heikinashi(dataframe)
|
||||
dataframe['ha_open'] = heikinashi['open']
|
||||
dataframe['ha_close'] = heikinashi['close']
|
||||
dataframe['ha_high'] = heikinashi['high']
|
||||
dataframe['ha_low'] = heikinashi['low']
|
||||
"""
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||
),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
Loading…
Reference in New Issue
Block a user